function initWebGLContext(
  canvas: HTMLCanvasElement
): WebGLRenderingContext | null {
  const gl =
    (canvas.getContext("webgl") as WebGLRenderingContext | null) ||
    (canvas.getContext("experimental-webgl") as WebGLRenderingContext | null);
  return gl;
}

function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): boolean {
  const dpr = Math.max(1, window.devicePixelRatio || 1);
  const displayWidth = Math.floor(canvas.clientWidth * dpr);
  const displayHeight = Math.floor(canvas.clientHeight * dpr);
  if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
    canvas.width = displayWidth;
    canvas.height = displayHeight;
    return true;
  }
  return false;
}

export const WebGLMap = observer(() => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const glRef = useRef<WebGLRenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const bufferRef = useRef<WebGLBuffer | null>(null);
  const pointProgramRef = useRef<WebGLProgram | null>(null);
  const pointBufferRef = useRef<WebGLBuffer | null>(null);
  const screenLineProgramRef = useRef<WebGLProgram | null>(null);
  const screenLineBufferRef = useRef<WebGLBuffer | null>(null);
  const attribsRef = useRef<{ a_pos: number } | null>(null);
  const uniformsRef = useRef<{
    u_cameraPos: WebGLUniformLocation | null;
    u_scale: WebGLUniformLocation | null;
    u_resolution: WebGLUniformLocation | null;
    u_color: WebGLUniformLocation | null;
  } | null>(null);

  const { routeData, stationData, stationDataEn, stationDataZh, sightData } =
    useMapData() as any;
  const {
    position,
    scale,
    setPosition,
    setScale,
    isAutoMode,
    setIsAutoMode,
    screenCenter,
    setScreenCenter,
    userActivityTimestamp,
    updateUserActivity,
  } = useTransform();

  const cameraAnimationStore = useCameraAnimationStore();

  const scaleLimitsRef = useRef({
    min: null as number | null,
    max: null as number | null,
  });

  useEffect(() => {
    if (
      routeData?.scale_min !== undefined &&
      routeData?.scale_max !== undefined
    ) {
      scaleLimitsRef.current = {
        min: routeData.scale_min / 10,
        max: routeData.scale_max / 10,
      };
    }
  }, [routeData?.scale_min, routeData?.scale_max]);

  const clampScale = useCallback((value: number) => {
    const { min, max } = scaleLimitsRef.current;

    if (min === null || max === null) {
      return value;
    }

    const clampedValue = Math.max(min, Math.min(max, value));

    return clampedValue;
  }, []);
  const { selectedLanguage } = useGeolocationStore();
  const positionRef = useRef(position);
  const scaleRef = useRef(scale);
  const setPositionRef = useRef(setPosition);
  const setScaleRef = useRef(setScale);

  useEffect(() => {
    setPositionRef.current = setPosition;
  }, [setPosition]);

  useEffect(() => {
    setScaleRef.current = setScale;
  }, [setScale]);

  useEffect(() => {
    if (routeData) {
    }
  }, [routeData]);

  useEffect(() => {
    positionRef.current = position;
  }, [position]);

  useEffect(() => {
    scaleRef.current = scale;
  }, [scale]);

  const rotationAngle = useMemo(() => {
    const deg = (routeData as any)?.rotate ?? 0;
    return (deg * Math.PI) / 180;
  }, [routeData]);

  const {
    position: animatedYellowDotPosition,
    animateTo: animateYellowDotTo,
    setPositionImmediate: setYellowDotPositionImmediate,
  } = useAnimatedPolarPosition(0, 0, 800);

  const routePath = useMemo(() => {
    if (!routeData?.path || routeData?.path.length === 0)
      return new Float32Array();
    const centerLat = routeData.center_latitude;
    const centerLon = routeData.center_longitude;
    if (centerLat === undefined || centerLon === undefined)
      return new Float32Array();

    const cos = Math.cos(rotationAngle);
    const sin = Math.sin(rotationAngle);

    const verts: number[] = [];
    for (const [lat, lon] of routeData.path) {
      const local = coordinatesToLocal(lat - centerLat, lon - centerLon);
      const x = local.x * UP_SCALE;
      const y = local.y * UP_SCALE;
      const rx = x * cos - y * sin;
      const ry = x * sin + y * cos;
      verts.push(rx, ry);
    }
    return new Float32Array(verts);
  }, [
    routeData?.path,
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
  ]);

  const transformedTramCoords = useMemo(() => {
    const centerLat = routeData?.center_latitude;
    const centerLon = routeData?.center_longitude;
    if (centerLat === undefined || centerLon === undefined) return null;

    const coords: any = apiStore?.context?.currentCoordinates;
    if (!coords) return null;

    const local = coordinatesToLocal(
      coords.latitude - centerLat,
      coords.longitude - centerLon
    );
    const x = local.x * UP_SCALE;
    const y = local.y * UP_SCALE;
    const cos = Math.cos(rotationAngle);
    const sin = Math.sin(rotationAngle);
    const rx = x * cos - y * sin;
    const ry = x * sin + y * cos;

    return { x: rx, y: ry };
  }, [
    routeData?.center_latitude,
    routeData?.center_longitude,
    apiStore?.context?.currentCoordinates,
    rotationAngle,
  ]);

  useEffect(() => {
    const callback = (newPos: { x: number; y: number }, newZoom: number) => {
      setPosition(newPos);
      setScale(newZoom);
    };

    cameraAnimationStore.setUpdateCallback(callback);

    cameraAnimationStore.syncState(position, scale);

    return () => {
      cameraAnimationStore.setUpdateCallback(null);
    };
  }, []);

  useEffect(() => {
    if (
      routeData?.scale_min !== undefined &&
      routeData?.scale_max !== undefined
    ) {
      cameraAnimationStore.setMaxZoom(routeData.scale_max / SCALE_FACTOR);
      cameraAnimationStore.setMinZoom(routeData.scale_min / SCALE_FACTOR);
    }
  }, [routeData?.scale_min, routeData?.scale_max, cameraAnimationStore]);

  useEffect(() => {
    const interval = setInterval(() => {
      const timeSinceActivity = Date.now() - userActivityTimestamp;
      if (timeSinceActivity >= 5000 && !isAutoMode) {
        setIsAutoMode(true);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [userActivityTimestamp, isAutoMode, setIsAutoMode]);

  useEffect(() => {
    if (cameraAnimationStore.isActivelyAnimating) {
      return;
    }

    if (isAutoMode && transformedTramCoords && screenCenter) {
      const transformedStations = stationData
        ? stationData
            .map((station: any) => {
              const centerLat = routeData?.center_latitude;
              const centerLon = routeData?.center_longitude;
              if (centerLat === undefined || centerLon === undefined)
                return null;

              const local = coordinatesToLocal(
                station.latitude - centerLat,
                station.longitude - centerLon
              );
              const x = local.x * UP_SCALE;
              const y = local.y * UP_SCALE;
              const cos = Math.cos(rotationAngle);
              const sin = Math.sin(rotationAngle);
              const rx = x * cos - y * sin;
              const ry = x * sin + y * cos;

              return {
                longitude: rx,
                latitude: ry,
                id: station.id,
              };
            })
            .filter(Boolean)
        : [];

      if (
        transformedTramCoords &&
        screenCenter &&
        transformedStations &&
        scaleLimitsRef.current !== null &&
        scaleLimitsRef.current.max !== null &&
        scaleLimitsRef.current.min &&
        scaleLimitsRef.current.min !== null
      ) {
        cameraAnimationStore.setMaxZoom(scaleLimitsRef.current!.max);
        cameraAnimationStore.setMinZoom(scaleLimitsRef.current!.min);

        cameraAnimationStore.syncState(positionRef.current, scaleRef.current);

        cameraAnimationStore.followTram(
          transformedTramCoords,
          screenCenter,
          transformedStations
        );
      }
    } else if (!isAutoMode) {
      cameraAnimationStore.stopAnimation();
    }
  }, [
    isAutoMode,
    transformedTramCoords,
    screenCenter,
    cameraAnimationStore,
    stationData,
    routeData,
    rotationAngle,
  ]);

  const stationLabels = useMemo(() => {
    if (!stationData || !routeData)
      return [] as Array<{ x: number; y: number; name: string; sub?: string }>;
    const centerLat = routeData.center_latitude;
    const centerLon = routeData.center_longitude;
    if (centerLat === undefined || centerLon === undefined) return [];
    const cos = Math.cos(rotationAngle);
    const sin = Math.sin(rotationAngle);
    const result: Array<{ x: number; y: number; name: string; sub?: string }> =
      [];
    for (let i = 0; i < stationData.length; i++) {
      const st = stationData[i];
      const local = coordinatesToLocal(
        st.latitude - centerLat,
        st.longitude - centerLon
      );
      const x = local.x * UP_SCALE;
      const y = local.y * UP_SCALE;
      const rx = x * cos - y * sin;
      const ry = x * sin + y * cos;
      const DEFAULT_LABEL_OFFSET_X = 25;
      const DEFAULT_LABEL_OFFSET_Y = 0;
      const labelOffsetX =
        st.offset_x === 0 && st.offset_y === 0
          ? DEFAULT_LABEL_OFFSET_X
          : st.offset_x;
      const labelOffsetY =
        st.offset_x === 0 && st.offset_y === 0
          ? DEFAULT_LABEL_OFFSET_Y
          : st.offset_y;
      const textBlockPositionX = rx + labelOffsetX;
      const textBlockPositionY = ry + labelOffsetY;
      const dpr = Math.max(
        1,
        (typeof window !== "undefined" && window.devicePixelRatio) || 1
      );
      const sx = (textBlockPositionX * scale + position.x) / dpr;
      const sy = (textBlockPositionY * scale + position.y) / dpr;
      let sub: string | undefined;
      if ((selectedLanguage as any) === "zh")
        sub = (stationDataZh as any)?.[i]?.name;
      else if (
        (selectedLanguage as any) === "en" ||
        (selectedLanguage as any) === "ru"
      )
        sub = (stationDataEn as any)?.[i]?.name;
      result.push({ x: sx, y: sy, name: st.name, sub });
    }
    return result;
  }, [
    stationData,
    stationDataEn as any,
    stationDataZh as any,
    position.x,
    position.y,
    scale,
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
    selectedLanguage as any,
  ]);

  const stationPoints = useMemo(() => {
    if (!stationData || !routeData) return new Float32Array();
    const centerLat = routeData.center_latitude;
    const centerLon = routeData.center_longitude;
    if (centerLat === undefined || centerLon === undefined)
      return new Float32Array();

    const cos = Math.cos(rotationAngle);
    const sin = Math.sin(rotationAngle);
    const verts: number[] = [];
    for (const s of stationData) {
      const local = coordinatesToLocal(
        s.latitude - centerLat,
        s.longitude - centerLon
      );
      const x = local.x * UP_SCALE;
      const y = local.y * UP_SCALE;
      const rx = x * cos - y * sin;
      const ry = x * sin + y * cos;
      verts.push(rx, ry);
    }
    return new Float32Array(verts);
  }, [
    stationData,
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
  ]);

  const sightPoints = useMemo(() => {
    if (!sightData || !routeData) return new Float32Array();
    const centerLat = routeData.center_latitude;
    const centerLon = routeData.center_longitude;
    if (centerLat === undefined || centerLon === undefined)
      return new Float32Array();

    const cos = Math.cos(rotationAngle);
    const sin = Math.sin(rotationAngle);
    const verts: number[] = [];
    for (const s of sightData) {
      const local = coordinatesToLocal(
        s.latitude - centerLat,
        s.longitude - centerLon
      );
      const x = local.x * UP_SCALE;
      const y = local.y * UP_SCALE;
      const rx = x * cos - y * sin;
      const ry = x * sin + y * cos;
      verts.push(rx, ry);
    }
    return new Float32Array(verts);
  }, [
    sightData,
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
  ]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const gl = initWebGLContext(canvas);
    glRef.current = gl;
    if (!gl) return;

    const vertSrc = `
      attribute vec2 a_pos;
      uniform vec2 u_cameraPos;
      uniform float u_scale;
      uniform vec2 u_resolution;
      void main() {
        vec2 screen = a_pos * u_scale + u_cameraPos;
        vec2 zeroToOne = screen / u_resolution;
        vec2 zeroToTwo = zeroToOne * 2.0;
        vec2 clip = zeroToTwo - 1.0;
        gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
      }
    `;
    const fragSrc = `
      precision mediump float;
      uniform vec4 u_color;
      void main() {
        gl_FragColor = u_color;
      }
    `;

    const compile = (type: number, src: string) => {
      const s = gl.createShader(type)!;
      gl.shaderSource(s, src);
      gl.compileShader(s);
      return s;
    };
    const vs = compile(gl.VERTEX_SHADER, vertSrc);
    const fs = compile(gl.FRAGMENT_SHADER, fragSrc);
    const prog = gl.createProgram()!;
    gl.attachShader(prog, vs);
    gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    programRef.current = prog;
    gl.useProgram(prog);

    const a_pos = gl.getAttribLocation(prog, "a_pos");
    const u_cameraPos = gl.getUniformLocation(prog, "u_cameraPos");
    const u_scale = gl.getUniformLocation(prog, "u_scale");
    const u_resolution = gl.getUniformLocation(prog, "u_resolution");
    const u_color = gl.getUniformLocation(prog, "u_color");
    attribsRef.current = { a_pos };
    uniformsRef.current = { u_cameraPos, u_scale, u_resolution, u_color };

    const buffer = gl.createBuffer();
    bufferRef.current = buffer;

    const pointVert = `
      attribute vec2 a_pos;
      uniform vec2 u_cameraPos;
      uniform float u_scale;
      uniform vec2 u_resolution;
      uniform float u_pointSize;
      void main() {
        vec2 screen = a_pos * u_scale + u_cameraPos;
        vec2 zeroToOne = screen / u_resolution;
        vec2 zeroToTwo = zeroToOne * 2.0;
        vec2 clip = zeroToTwo - 1.0;
        gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
        gl_PointSize = u_pointSize;
      }
    `;
    const pointFrag = `
      precision mediump float;
      uniform vec4 u_color;
      void main() {
        vec2 c = gl_PointCoord * 2.0 - 1.0;
        float d = dot(c, c);
        if (d > 1.0) discard;
        gl_FragColor = u_color;
      }
    `;
    const vs2 = compile(gl.VERTEX_SHADER, pointVert);
    const fs2 = compile(gl.FRAGMENT_SHADER, pointFrag);
    const pprog = gl.createProgram()!;
    gl.attachShader(pprog, vs2);
    gl.attachShader(pprog, fs2);
    gl.linkProgram(pprog);
    pointProgramRef.current = pprog;
    pointBufferRef.current = gl.createBuffer();

    const lineVert = `
      attribute vec2 a_screen;
      uniform vec2 u_resolution;
      void main(){
        vec2 zeroToOne = a_screen / u_resolution;
        vec2 clip = zeroToOne * 2.0 - 1.0;
        gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
      }
    `;
    const lineFrag = `
      precision mediump float;
      uniform vec4 u_color;
      void main(){ gl_FragColor = u_color; }
    `;
    const lv = compile(gl.VERTEX_SHADER, lineVert);
    const lf = compile(gl.FRAGMENT_SHADER, lineFrag);
    const lprog = gl.createProgram()!;
    gl.attachShader(lprog, lv);
    gl.attachShader(lprog, lf);
    gl.linkProgram(lprog);
    screenLineProgramRef.current = lprog;
    screenLineBufferRef.current = gl.createBuffer();

    const handleResize = () => {
      const changed = resizeCanvasToDisplaySize(canvas);
      if (!gl) return;
      setScreenCenter({
        x: canvas.width / 2,
        y: canvas.height / 2,
      });
      if (changed) {
        gl.viewport(0, 0, canvas.width, canvas.height);
      }
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
    };

    handleResize();
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  useEffect(() => {
    const centerLat = routeData?.center_latitude;
    const centerLon = routeData?.center_longitude;
    if (centerLat !== undefined && centerLon !== undefined) {
      const coords: any = apiStore?.context?.currentCoordinates;
      if (coords) {
        const local = coordinatesToLocal(
          coords.latitude - centerLat,
          coords.longitude - centerLon
        );
        const x = local.x * UP_SCALE;
        const y = local.y * UP_SCALE;
        const cos = Math.cos(rotationAngle),
          sin = Math.sin(rotationAngle);
        const rx = x * cos - y * sin;
        const ry = x * sin + y * cos;

        if (isAutoMode) {
          animateYellowDotTo(rx, ry);
        } else {
          setYellowDotPositionImmediate(rx, ry);
        }
      }
    }
  }, [
    routeData?.center_latitude,
    routeData?.center_longitude,
    rotationAngle,
    apiStore?.context?.currentCoordinates?.latitude,
    apiStore?.context?.currentCoordinates?.longitude,
    isAutoMode,
    animateYellowDotTo,
    setYellowDotPositionImmediate,
  ]);

  useEffect(() => {
    const gl = glRef.current;
    const canvas = canvasRef.current;
    const prog = programRef.current;
    const buffer = bufferRef.current;
    const attribs = attribsRef.current;
    const uniforms = uniformsRef.current;
    const pprog = pointProgramRef.current;
    const pbuffer = pointBufferRef.current;
    if (
      !gl ||
      !canvas ||
      !prog ||
      !buffer ||
      !attribs ||
      !uniforms ||
      !pprog ||
      !pbuffer
    )
      return;

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(prog);
    gl.uniform2f(uniforms.u_cameraPos, position.x, position.y);
    gl.uniform1f(uniforms.u_scale, scale);
    gl.uniform2f(uniforms.u_resolution, canvas.width, canvas.height);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, routePath, gl.STATIC_DRAW);
    gl.enableVertexAttribArray(attribs.a_pos);
    gl.vertexAttribPointer(attribs.a_pos, 2, gl.FLOAT, false, 0, 0);

    const vcount = routePath.length / 2;
    let tramSegIndex = -1;
    {
      const centerLatTmp = routeData?.center_latitude;
      const centerLonTmp = routeData?.center_longitude;
      if (centerLatTmp !== undefined && centerLonTmp !== undefined) {
        const coordsAny: any = apiStore?.context?.currentCoordinates;
        if (coordsAny) {
          const loc = coordinatesToLocal(
            coordsAny.latitude - centerLatTmp,
            coordsAny.longitude - centerLonTmp
          );
          const wx = loc.x * UP_SCALE;
          const wy = loc.y * UP_SCALE;
          const cosR = Math.cos(rotationAngle),
            sinR = Math.sin(rotationAngle);
          const tX = wx * cosR - wy * sinR;
          const tY = wx * sinR + wy * cosR;
          let best = -1,
            bestD = Infinity;
          for (let i = 0; i < vcount - 1; i++) {
            const p1x = routePath[i * 2],
              p1y = routePath[i * 2 + 1];
            const p2x = routePath[(i + 1) * 2],
              p2y = routePath[(i + 1) * 2 + 1];
            const dx = p2x - p1x,
              dy = p2y - p1y;
            const len2 = dx * dx + dy * dy;
            if (!len2) continue;
            const t = ((tX - p1x) * dx + (tY - p1y) * dy) / len2;
            const cl = Math.max(0, Math.min(1, t));
            const px = p1x + cl * dx,
              py = p1y + cl * dy;
            const d = Math.hypot(tX - px, tY - py);
            if (d < bestD) {
              bestD = d;
              best = i;
            }
          }
          tramSegIndex = best;
        }
      }
    }

    const vertexCount = routePath.length / 2;
    if (vertexCount > 1) {
      const generateThickLine = (points: Float32Array, width: number) => {
        const vertices: number[] = [];
        const halfWidth = width / 2;

        if (points.length < 4) return new Float32Array();

        for (let i = 0; i < points.length - 2; i += 2) {
          const x1 = points[i];
          const y1 = points[i + 1];
          const x2 = points[i + 2];
          const y2 = points[i + 3];

          const dx = x2 - x1;
          const dy = y2 - y1;
          const length = Math.sqrt(dx * dx + dy * dy);
          if (length === 0) continue;

          const perpX = (-dy / length) * halfWidth;
          const perpY = (dx / length) * halfWidth;

          vertices.push(x1 + perpX, y1 + perpY);
          vertices.push(x1 - perpX, y1 - perpY);
          vertices.push(x2 + perpX, y2 + perpY);

          vertices.push(x1 - perpX, y1 - perpY);
          vertices.push(x2 - perpX, y2 - perpY);
          vertices.push(x2 + perpX, y2 + perpY);

          if (i < points.length - 4) {
            const x3 = points[i + 4];
            const y3 = points[i + 5];

            const dx2 = x3 - x2;
            const dy2 = y3 - y2;
            const length2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
            if (length2 > 0) {
              const perpX2 = (-dy2 / length2) * halfWidth;
              const perpY2 = (dx2 / length2) * halfWidth;

              vertices.push(x2 + perpX, y2 + perpY);
              vertices.push(x2 - perpX, y2 - perpY);
              vertices.push(x2 + perpX2, y2 + perpY2);

              vertices.push(x2 - perpX, y2 - perpY);
              vertices.push(x2 - perpX2, y2 - perpY2);
              vertices.push(x2 + perpX2, y2 + perpY2);
            }
          }
        }

        return new Float32Array(vertices);
      };

      const lineWidth = Math.min(6);
      const r1 = ((PATH_COLOR >> 16) & 0xff) / 255;
      const g1 = ((PATH_COLOR >> 8) & 0xff) / 255;
      const b1 = (PATH_COLOR & 0xff) / 255;
      gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);

      if (tramSegIndex >= 0) {
        const animatedPos = animatedYellowDotPosition;
        if (
          animatedPos &&
          animatedPos.x !== undefined &&
          animatedPos.y !== undefined
        ) {
          const passedPoints: number[] = [];

          for (let i = 0; i <= tramSegIndex; i++) {
            passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
          }

          passedPoints.push(animatedPos.x, animatedPos.y);

          if (passedPoints.length >= 4) {
            const thickLineVertices = generateThickLine(
              new Float32Array(passedPoints),
              lineWidth
            );
            gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
            gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
          }
        }
      }

      const r2 = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
      const g2 = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
      const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255;
      gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);

      const animatedPos = animatedYellowDotPosition;
      if (
        animatedPos &&
        animatedPos.x !== undefined &&
        animatedPos.y !== undefined
      ) {
        const unpassedPoints: number[] = [];

        unpassedPoints.push(animatedPos.x, animatedPos.y);

        for (let i = tramSegIndex + 1; i < vertexCount; i++) {
          unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
        }

        if (unpassedPoints.length >= 4) {
          const thickLineVertices = generateThickLine(
            new Float32Array(unpassedPoints),
            lineWidth
          );
          gl.bufferData(gl.ARRAY_BUFFER, thickLineVertices, gl.STATIC_DRAW);
          gl.drawArrays(gl.TRIANGLES, 0, thickLineVertices.length / 2);
        }
      }
    }

    if (stationPoints.length > 0) {
      gl.useProgram(pprog);
      const a_pos_pts = gl.getAttribLocation(pprog, "a_pos");
      const u_cameraPos_pts = gl.getUniformLocation(pprog, "u_cameraPos");
      const u_scale_pts = gl.getUniformLocation(pprog, "u_scale");
      const u_resolution_pts = gl.getUniformLocation(pprog, "u_resolution");
      const u_pointSize = gl.getUniformLocation(pprog, "u_pointSize");
      const u_color_pts = gl.getUniformLocation(pprog, "u_color");

      gl.uniform2f(u_cameraPos_pts, position.x, position.y);
      gl.uniform1f(u_scale_pts, scale);
      gl.uniform2f(u_resolution_pts, canvas.width, canvas.height);
      gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
      gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
      gl.enableVertexAttribArray(a_pos_pts);
      gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);

      gl.uniform1f(u_pointSize, 10 * scale * 1.5);
      const r_outline = ((BACKGROUND_COLOR >> 16) & 0xff) / 255;
      const g_outline = ((BACKGROUND_COLOR >> 8) & 0xff) / 255;
      const b_outline = (BACKGROUND_COLOR & 0xff) / 255;
      gl.uniform4f(u_color_pts, r_outline, g_outline, b_outline, 1);
      gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);

      gl.uniform1f(u_pointSize, 8.0 * scale * 1.5);

      if (tramSegIndex >= 0) {
        const passedStations = [];
        for (let i = 0; i < stationData.length; i++) {
          if (i <= tramSegIndex) {
            passedStations.push(stationPoints[i * 2], stationPoints[i * 2 + 1]);
          }
        }
        if (passedStations.length > 0) {
          const r_passed = ((PATH_COLOR >> 16) & 0xff) / 255;
          const g_passed = ((PATH_COLOR >> 8) & 0xff) / 255;
          const b_passed = (PATH_COLOR & 0xff) / 255;
          gl.uniform4f(u_color_pts, r_passed, g_passed, b_passed, 1);
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(passedStations),
            gl.STATIC_DRAW
          );
          gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
        }
      }

      if (tramSegIndex >= 0) {
        const unpassedStations = [];
        for (let i = 0; i < stationData.length; i++) {
          if (i > tramSegIndex) {
            unpassedStations.push(
              stationPoints[i * 2],
              stationPoints[i * 2 + 1]
            );
          }
        }
        if (unpassedStations.length > 0) {
          const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
          const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
          const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
          gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1);
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(unpassedStations),
            gl.STATIC_DRAW
          );
          gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
        }
      } else {
        const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
        const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
        const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
        gl.uniform4f(u_color_pts, r_unpassed, g_unpassed, b_unpassed, 1);
        gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
        gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
      }
    }

    const a_pos_pts = gl.getAttribLocation(pprog, "a_pos");
    const u_cameraPos_pts = gl.getUniformLocation(pprog, "u_cameraPos");
    const u_scale_pts = gl.getUniformLocation(pprog, "u_scale");
    const u_resolution_pts = gl.getUniformLocation(pprog, "u_resolution");
    const u_pointSize = gl.getUniformLocation(pprog, "u_pointSize");
    const u_color_pts = gl.getUniformLocation(pprog, "u_color");

    gl.uniform2f(u_cameraPos_pts, position.x, position.y);
    gl.uniform1f(u_scale_pts, scale);
    gl.uniform2f(u_resolution_pts, canvas.width, canvas.height);

    const toPointsArray = (arr: number[]) => new Float32Array(arr);

    const pathPts: { x: number; y: number }[] = [];
    for (let i = 0; i < routePath.length; i += 2)
      pathPts.push({ x: routePath[i], y: routePath[i + 1] });
    const getSeg = (px: number, py: number) => {
      if (pathPts.length < 2) return -1;
      let best = -1,
        bestD = Infinity;
      for (let i = 0; i < pathPts.length - 1; i++) {
        const p1 = pathPts[i],
          p2 = pathPts[i + 1];
        const dx = p2.x - p1.x,
          dy = p2.y - p1.y;
        const len2 = dx * dx + dy * dy;
        if (!len2) continue;
        const t = ((px - p1.x) * dx + (py - p1.y) * dy) / len2;
        const tt = Math.max(0, Math.min(1, t));
        const cx = p1.x + tt * dx,
          cy = p1.y + tt * dy;
        const d = Math.hypot(px - cx, py - cy);
        if (d < bestD) {
          bestD = d;
          best = i;
        }
      }
      return best;
    };

    let tramSegForStations = -1;
    {
      const cLat = routeData?.center_latitude,
        cLon = routeData?.center_longitude;
      const tram = apiStore?.context?.currentCoordinates as any;
      if (tram && cLat !== undefined && cLon !== undefined) {
        const loc = coordinatesToLocal(
          tram.latitude - cLat,
          tram.longitude - cLon
        );
        const wx = loc.x * UP_SCALE,
          wy = loc.y * UP_SCALE;
        const cosR = Math.cos(rotationAngle),
          sinR = Math.sin(rotationAngle);
        const tx = wx * cosR - wy * sinR,
          ty = wx * sinR + wy * cosR;
        tramSegForStations = getSeg(tx, ty);
      }
    }

    const passedStations: number[] = [];
    const unpassedStations: number[] = [];
    for (let i = 0; i < stationPoints.length; i += 2) {
      const sx = stationPoints[i],
        sy = stationPoints[i + 1];
      const seg = getSeg(sx, sy);
      if (tramSegForStations !== -1 && seg !== -1 && seg < tramSegForStations)
        passedStations.push(sx, sy);
      else unpassedStations.push(sx, sy);
    }

    const outlineSize = 10.0 * scale * 2,
      coreSize = 8.0 * scale * 2;

    gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      toPointsArray(unpassedStations),
      gl.STREAM_DRAW
    );
    gl.enableVertexAttribArray(a_pos_pts);
    gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
    gl.uniform1f(u_pointSize, outlineSize);
    gl.uniform4f(
      u_color_pts,
      ((BACKGROUND_COLOR >> 16) & 255) / 255,
      ((BACKGROUND_COLOR >> 8) & 255) / 255,
      (BACKGROUND_COLOR & 255) / 255,
      1
    );
    if (unpassedStations.length)
      gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
    gl.uniform1f(u_pointSize, coreSize);
    gl.uniform4f(
      u_color_pts,
      ((UNPASSED_STATION_COLOR >> 16) & 255) / 255,
      ((UNPASSED_STATION_COLOR >> 8) & 255) / 255,
      (UNPASSED_STATION_COLOR & 255) / 255,
      1
    );
    if (unpassedStations.length)
      gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);

    gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      toPointsArray(passedStations),
      gl.STREAM_DRAW
    );
    gl.enableVertexAttribArray(a_pos_pts);
    gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
    gl.uniform1f(u_pointSize, outlineSize);
    gl.uniform4f(
      u_color_pts,
      ((BACKGROUND_COLOR >> 16) & 255) / 255,
      ((BACKGROUND_COLOR >> 8) & 255) / 255,
      (BACKGROUND_COLOR & 255) / 255,
      1
    );
    if (passedStations.length)
      gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
    gl.uniform1f(u_pointSize, coreSize);
    gl.uniform4f(
      u_color_pts,
      ((PATH_COLOR >> 16) & 255) / 255,
      ((PATH_COLOR >> 8) & 255) / 255,
      (PATH_COLOR & 255) / 255,
      1
    );
    if (passedStations.length)
      gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);

    if (
      stationData &&
      stationData.length > 0 &&
      routeData &&
      apiStore?.context
    ) {
      const centerLat = routeData.center_latitude;
      const centerLon = routeData.center_longitude;
      if (centerLat !== undefined && centerLon !== undefined) {
        const cos = Math.cos(rotationAngle);
        const sin = Math.sin(rotationAngle);

        const startStationData = stationData.find(
          (station) => station.id.toString() === apiStore.context?.startStopId
        );
        const endStationData = stationData.find(
          (station) => station.id.toString() === apiStore.context?.endStopId
        );

        const terminalStations: number[] = [];

        if (startStationData) {
          const startLocal = coordinatesToLocal(
            startStationData.latitude - centerLat,
            startStationData.longitude - centerLon
          );
          const startX = startLocal.x * UP_SCALE;
          const startY = startLocal.y * UP_SCALE;
          const startRx = startX * cos - startY * sin;
          const startRy = startX * sin + startY * cos;
          terminalStations.push(startRx, startRy);
        }

        if (endStationData) {
          const endLocal = coordinatesToLocal(
            endStationData.latitude - centerLat,
            endStationData.longitude - centerLon
          );
          const endX = endLocal.x * UP_SCALE;
          const endY = endLocal.y * UP_SCALE;
          const endRx = endX * cos - endY * sin;
          const endRy = endX * sin + endY * cos;
          terminalStations.push(endRx, endRy);
        }

        if (terminalStations.length > 0) {
          const terminalStationData: any[] = [];
          if (startStationData) terminalStationData.push(startStationData);
          if (endStationData) terminalStationData.push(endStationData);

          let tramSegIndex = -1;
          const coords: any = apiStore?.context?.currentCoordinates;
          if (coords && centerLat !== undefined && centerLon !== undefined) {
            const local = coordinatesToLocal(
              coords.latitude - centerLat,
              coords.longitude - centerLon
            );
            const wx = local.x * UP_SCALE;
            const wy = local.y * UP_SCALE;
            const cosR = Math.cos(rotationAngle);
            const sinR = Math.sin(rotationAngle);
            const tx = wx * cosR - wy * sinR;
            const ty = wx * sinR + wy * cosR;

            let best = -1;
            let bestD = Infinity;
            for (let i = 0; i < routePath.length - 2; i += 2) {
              const p1x = routePath[i];
              const p1y = routePath[i + 1];
              const p2x = routePath[i + 2];
              const p2y = routePath[i + 3];
              const dx = p2x - p1x;
              const dy = p2y - p1y;
              const len2 = dx * dx + dy * dy;
              if (!len2) continue;
              const t = ((tx - p1x) * dx + (ty - p1y) * dy) / len2;
              const cl = Math.max(0, Math.min(1, t));
              const px = p1x + cl * dx;
              const py = p1y + cl * dy;
              const d = Math.hypot(tx - px, ty - py);
              if (d < bestD) {
                bestD = d;
                best = i / 2;
              }
            }
            tramSegIndex = best;
          }

          const isStartPassed = startStationData
            ? (() => {
                const sx = terminalStations[0];
                const sy = terminalStations[1];
                const seg = (() => {
                  if (routePath.length < 4) return -1;
                  let best = -1;
                  let bestD = Infinity;
                  for (let i = 0; i < routePath.length - 2; i += 2) {
                    const p1x = routePath[i];
                    const p1y = routePath[i + 1];
                    const p2x = routePath[i + 2];
                    const p2y = routePath[i + 3];
                    const dx = p2x - p1x;
                    const dy = p2y - p1y;
                    const len2 = dx * dx + dy * dy;
                    if (!len2) continue;
                    const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2;
                    const cl = Math.max(0, Math.min(1, t));
                    const px = p1x + cl * dx;
                    const py = p1y + cl * dy;
                    const d = Math.hypot(sx - px, sy - py);
                    if (d < bestD) {
                      bestD = d;
                      best = i / 2;
                    }
                  }
                  return best;
                })();
                return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex;
              })()
            : false;

          const isEndPassed = endStationData
            ? (() => {
                const ex = terminalStations[terminalStations.length - 2];
                const ey = terminalStations[terminalStations.length - 1];
                const seg = (() => {
                  if (routePath.length < 4) return -1;
                  let best = -1;
                  let bestD = Infinity;
                  for (let i = 0; i < routePath.length - 2; i += 2) {
                    const p1x = routePath[i];
                    const p1y = routePath[i + 1];
                    const p2x = routePath[i + 2];
                    const p2y = routePath[i + 3];
                    const dx = p2x - p1x;
                    const dy = p2y - p1y;
                    const len2 = dx * dx + dy * dy;
                    if (!len2) continue;
                    const t = ((ex - p1x) * dx + (ey - p1y) * dy) / len2;
                    const cl = Math.max(0, Math.min(1, t));
                    const px = p1x + cl * dx;
                    const py = p1y + cl * dy;
                    const d = Math.hypot(ex - px, ey - py);
                    if (d < bestD) {
                      bestD = d;
                      best = i / 2;
                    }
                  }
                  return best;
                })();
                return tramSegIndex !== -1 && seg !== -1 && seg < tramSegIndex;
              })()
            : false;

          gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(terminalStations),
            gl.STREAM_DRAW
          );
          gl.enableVertexAttribArray(a_pos_pts);
          gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);

          gl.uniform1f(u_pointSize, 18.0 * scale);
          if (startStationData && endStationData) {
            if (isStartPassed) {
              gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0);
            } else {
              gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0);
            }
            gl.drawArrays(gl.POINTS, 0, 1);

            if (isEndPassed) {
              gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0);
            } else {
              gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0);
            }
            gl.drawArrays(gl.POINTS, 1, 1);
          } else {
            const isPassed = startStationData ? isStartPassed : isEndPassed;
            if (isPassed) {
              gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0);
            } else {
              gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0);
            }
            gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
          }

          gl.uniform1f(u_pointSize, 11.0 * scale);
          const r_center = ((BACKGROUND_COLOR >> 16) & 0xff) / 255;
          const g_center = ((BACKGROUND_COLOR >> 8) & 0xff) / 255;
          const b_center = (BACKGROUND_COLOR & 0xff) / 255;
          gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0);
          gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
        }
      }
    }

    if (animatedYellowDotPosition) {
      const rx = animatedYellowDotPosition.x;
      const ry = animatedYellowDotPosition.y;

      gl.uniform1f(u_pointSize, 13.3333 * scale);
      gl.uniform4f(u_color_pts, 1.0, 1.0, 0.0, 1.0);
      const tmp = new Float32Array([rx, ry]);
      gl.bindBuffer(gl.ARRAY_BUFFER, pbuffer);
      gl.bufferData(gl.ARRAY_BUFFER, tmp, gl.STREAM_DRAW);
      gl.enableVertexAttribArray(a_pos_pts);
      gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
      gl.drawArrays(gl.POINTS, 0, 1);
    }
  }, [
    routePath,
    stationPoints,
    sightPoints,
    position.x,
    position.y,
    scale,
    animatedYellowDotPosition?.x,
    animatedYellowDotPosition?.y,
  ]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    if (!routePath || routePath.length < 4) return;

    let minX = Infinity,
      minY = Infinity,
      maxX = -Infinity,
      maxY = -Infinity;
    for (let i = 0; i < routePath.length; i += 2) {
      const x = routePath[i];
      const y = routePath[i + 1];
      if (x < minX) minX = x;
      if (y < minY) minY = y;
      if (x > maxX) maxX = x;
      if (y > maxY) maxY = y;
    }
    if (
      !isFinite(minX) ||
      !isFinite(minY) ||
      !isFinite(maxX) ||
      !isFinite(maxY)
    )
      return;

    const worldWidth = Math.max(1, maxX - minX);
    const worldHeight = Math.max(1, maxY - minY);

    const margin = 0.1;
    const targetScale = Math.min(
      (canvas.width * (1 - margin)) / worldWidth,
      (canvas.height * (1 - margin)) / worldHeight
    );

    const centerX = (minX + maxX) / 2;
    const centerY = (minY + maxY) / 2;

    const clampedScale = clampScale(targetScale);

    setScaleRef.current(clampedScale);
    setPositionRef.current({
      x: canvas.width / 2 - centerX * clampedScale,
      y: canvas.height / 2 - centerY * clampedScale,
    });
  }, [routePath]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    let isDragging = false;
    let startMouse = { x: 0, y: 0 };
    let startPos = { x: 0, y: 0 };

    const activePointers = new Map<number, { x: number; y: number }>();
    let isPinching = false;
    let pinchStart: {
      distance: number;
      midpoint: { x: number; y: number };
      scale: number;
      position: { x: number; y: number };
    } | null = null;

    const getDistance = (
      p1: { x: number; y: number },
      p2: { x: number; y: number }
    ) => Math.hypot(p2.x - p1.x, p2.y - p1.y);

    const getMidpoint = (
      p1: { x: number; y: number },
      p2: { x: number; y: number }
    ) => ({
      x: (p1.x + p2.x) / 2,
      y: (p1.y + p2.y) / 2,
    });

    const onPointerDown = (e: PointerEvent) => {
      updateUserActivity();
      if (isAutoMode) {
        setIsAutoMode(false);
      }
      cameraAnimationStore.stopAnimation();

      canvas.setPointerCapture(e.pointerId);
      const rect = canvas.getBoundingClientRect();
      activePointers.set(e.pointerId, {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top,
      });
      if (activePointers.size === 1) {
        isDragging = true;
        startMouse = { x: e.clientX - rect.left, y: e.clientY - rect.top };
        startPos = { x: positionRef.current.x, y: positionRef.current.y };
      } else if (activePointers.size === 2) {
        isDragging = false;
        const [p1, p2] = Array.from(activePointers.values());
        pinchStart = {
          distance: getDistance(p1, p2),
          midpoint: getMidpoint(p1, p2),
          scale: scaleRef.current,
          position: { x: positionRef.current.x, y: positionRef.current.y },
        };
        isPinching = true;
      }
    };

    const onPointerMove = (e: PointerEvent) => {
      if (!activePointers.has(e.pointerId)) return;

      updateUserActivity();

      const rect = canvas.getBoundingClientRect();
      activePointers.set(e.pointerId, {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top,
      });

      if (activePointers.size === 2) {
        isDragging = false;

        const pointersArray = Array.from(activePointers.values());
        if (pointersArray.length === 2) {
          const [p1, p2] = pointersArray;

          if (!isPinching || pinchStart === null) {
            isPinching = true;
            pinchStart = {
              distance: getDistance(p1, p2),
              midpoint: getMidpoint(p1, p2),
              scale: scaleRef.current,
              position: { x: positionRef.current.x, y: positionRef.current.y },
            };
          }

          if (pinchStart) {
            const currentDistance = getDistance(p1, p2);
            const zoomFactor = currentDistance / pinchStart.distance;
            const unclampedScale = pinchStart.scale * zoomFactor;
            const newScale = clampScale(Math.max(0.1, unclampedScale));

            const k = newScale / pinchStart.scale;
            const newPosition = {
              x: pinchStart.position.x * k + pinchStart.midpoint.x * (1 - k),
              y: pinchStart.position.y * k + pinchStart.midpoint.y * (1 - k),
            };
            setPositionRef.current(newPosition);
            setScaleRef.current(newScale);
          }
        }
      } else if (isDragging && activePointers.size === 1) {
        const p = Array.from(activePointers.values())[0];

        if (
          !startMouse ||
          !startPos ||
          typeof startMouse.x !== "number" ||
          typeof startMouse.y !== "number" ||
          typeof startPos.x !== "number" ||
          typeof startPos.y !== "number"
        ) {
          console.warn(
            "WebGLMap: Некорректные значения startMouse или startPos:",
            {
              startMouse,
              startPos,
              p,
            }
          );
          return;
        }

        const dx = p.x - startMouse.x;
        const dy = p.y - startMouse.y;

        setPositionRef.current({ x: startPos.x + dx, y: startPos.y + dy });
      }
    };

    const onPointerUp = (e: PointerEvent) => {
      updateUserActivity();

      canvas.releasePointerCapture(e.pointerId);
      activePointers.delete(e.pointerId);
      if (activePointers.size < 2) {
        isPinching = false;
        pinchStart = null;
      }
      if (activePointers.size === 0) {
        isDragging = false;
      } else if (activePointers.size === 1) {
        const p = Array.from(activePointers.values())[0];
        startPos = { x: positionRef.current.x, y: positionRef.current.y };
        startMouse = { x: p.x, y: p.y };
        isDragging = true;
      }
    };

    const onPointerCancel = (e: PointerEvent) => {
      updateUserActivity();
      canvas.releasePointerCapture(e.pointerId);
      activePointers.delete(e.pointerId);
      isPinching = false;
      pinchStart = null;
      if (activePointers.size === 0) {
        isDragging = false;
      }
    };

    const onWheel = (e: WheelEvent) => {
      e.preventDefault();

      updateUserActivity();
      if (isAutoMode) {
        setIsAutoMode(false);
      }
      cameraAnimationStore.stopAnimation();

      const rect = canvas.getBoundingClientRect();
      const mouseX =
        (e.clientX - rect.left) * (canvas.width / canvas.clientWidth);
      const mouseY =
        (e.clientY - rect.top) * (canvas.height / canvas.clientHeight);
      const delta = e.deltaY > 0 ? 0.9 : 1.1;
      const unclampedScale = scaleRef.current * delta;
      const newScale = clampScale(Math.max(0.1, unclampedScale));

      const k = newScale / scaleRef.current;
      const newPosition = {
        x: positionRef.current.x * k + mouseX * (1 - k),
        y: positionRef.current.y * k + mouseY * (1 - k),
      };
      setScaleRef.current(newScale);
      setPositionRef.current(newPosition);
    };

    canvas.addEventListener("pointerdown", onPointerDown);
    canvas.addEventListener("pointermove", onPointerMove);
    canvas.addEventListener("pointerup", onPointerUp);
    canvas.addEventListener("pointercancel", onPointerCancel);
    canvas.addEventListener("pointerleave", onPointerUp);
    canvas.addEventListener("wheel", onWheel, { passive: false });

    return () => {
      canvas.removeEventListener("pointerdown", onPointerDown);
      canvas.removeEventListener("pointermove", onPointerMove);
      canvas.removeEventListener("pointerup", onPointerUp);
      canvas.removeEventListener("pointercancel", onPointerCancel);
      canvas.removeEventListener("pointerleave", onPointerUp);
      canvas.removeEventListener("wheel", onWheel as any);
    };
  }, [
    updateUserActivity,
    setIsAutoMode,
    cameraAnimationStore,
    isAutoMode,
    clampScale,
  ]);

  return (
    <div
      className="map-layer"
      style={{ width: "100%", height: "100vh", position: "relative" }}
    >
      <canvas
        ref={canvasRef}
        style={{ width: "100%", height: "100%", display: "block", zIndex: 0 }}
      />
      <div
        style={{
          position: "absolute",
          inset: 0,
          pointerEvents: "none",
          zIndex: 9999,
        }}
      >
        {stationLabels.map((l, idx) => (
          <div
            key={idx}
            style={{
              position: "absolute",
              left: l.x,
              top: l.y,
              transform: "translate(0, -50%)",
              color: "#fff",
              fontFamily: "Roboto",
            }}
          >
            <div style={{ fontWeight: 700, fontSize: 16 }}>{l.name}</div>
            {l.sub ? (
              <div
                style={{
                  color: "#CBCBCB",
                  fontWeight: 400,
                  fontSize: 13,
                  marginTop: 2,
                }}
              >
                {l.sub}
              </div>
            ) : null}
          </div>
        ))}
        {sightData?.map((s: any, i: number) => {
          const centerLat = routeData?.center_latitude;
          const centerLon = routeData?.center_longitude;
          if (centerLat === undefined || centerLon === undefined) return null;
          const cos = Math.cos(rotationAngle);
          const sin = Math.sin(rotationAngle);
          const local = coordinatesToLocal(
            s.latitude - centerLat,
            s.longitude - centerLon
          );
          const x = local.x * UP_SCALE;
          const y = local.y * UP_SCALE;
          const rx = x * cos - y * sin;
          const ry = x * sin + y * cos;
          const dpr = Math.max(
            1,
            (typeof window !== "undefined" && window.devicePixelRatio) || 1
          );
          const sx = (rx * scale + position.x) / dpr;
          const sy = (ry * scale + position.y) / dpr;
          const size = 30;

          const handleSightClick = () => {
            const {
              setSelectedSightId,
              setIsManualSelection,
              setIsRightWidgetSelectorOpen,
              closeGovernorModal,
            } = useGeolocationStore();
            setSelectedSightId(String(s.id));
            setIsManualSelection(true);
            setIsRightWidgetSelectorOpen(false);
            closeGovernorModal();
          };

          return (
            <img
              key={`sight-${s.id}`}
              src={sightIcon}
              alt=""
              width={size}
              height={size}
              style={{
                position: "absolute",
                left: sx - size / 2,
                top: sy - size / 2,
                pointerEvents: "auto",
                cursor: "pointer",
              }}
              onClick={handleSightClick}
            />
          );
        })}

        {(() => {
          if (!routeData) return null;
          const centerLat = routeData.center_latitude;
          const centerLon = routeData.center_longitude;
          if (centerLat === undefined || centerLon === undefined) return null;

          const coords: any = apiStore?.context?.currentCoordinates;
          if (!coords) return null;

          const local = coordinatesToLocal(
            coords.latitude - centerLat,
            coords.longitude - centerLon
          );
          const wx = local.x * UP_SCALE;
          const wy = local.y * UP_SCALE;
          const cosR = Math.cos(rotationAngle);
          const sinR = Math.sin(rotationAngle);
          const rx = wx * cosR - wy * sinR;
          const ry = wx * sinR + wy * cosR;
          const dpr2 = Math.max(
            1,
            (typeof window !== "undefined" && window.devicePixelRatio) || 1
          );
          const screenX = (rx * scale + position.x) / dpr2;
          const screenY = (ry * scale + position.y) / dpr2;

          const pathPts: { x: number; y: number }[] = [];
          for (let i = 0; i < routePath.length; i += 2)
            pathPts.push({ x: routePath[i], y: routePath[i + 1] });
          const stationsForAngle = (stationData || []).map((st: any) => {
            const loc = coordinatesToLocal(
              st.latitude - centerLat,
              st.longitude - centerLon
            );
            const x = loc.x * UP_SCALE,
              y = loc.y * UP_SCALE;
            const rx2 = x * cosR - y * sinR,
              ry2 = x * sinR + y * cosR;
            return {
              longitude: rx2,
              latitude: ry2,
              offset_x: st.offset_x,
              offset_y: st.offset_y,
            };
          });
          let tramSegIndex = -1;
          if (routePath.length >= 4) {
            let best = -1,
              bestD = Infinity;
            for (let i = 0; i < routePath.length - 2; i += 2) {
              const p1x = routePath[i],
                p1y = routePath[i + 1];
              const p2x = routePath[i + 2],
                p2y = routePath[i + 3];
              const dx = p2x - p1x,
                dy = p2y - p1y;
              const len2 = dx * dx + dy * dy;
              if (!len2) continue;
              const t = ((rx - p1x) * dx + (ry - p1y) * dy) / len2;
              const cl = Math.max(0, Math.min(1, t));
              const px = p1x + cl * dx,
                py = p1y + cl * dy;
              const d = Math.hypot(rx - px, ry - py);
              if (d < bestD) {
                bestD = d;
                best = i / 2;
              }
            }
            tramSegIndex = best;
          }
          const optimalAngle = (() => {
            const testRadiusInMap = 100 / scale;
            const minPath = 60,
              minPassed = 60,
              minStation = 50;
            let bestAng = 0,
              bestScore = Infinity;
            for (let i = 0; i < 12; i++) {
              const ang = (i * Math.PI * 2) / 12;
              const tx = rx + Math.cos(ang) * testRadiusInMap;
              const ty = ry + Math.sin(ang) * testRadiusInMap;
              const distPath = (function () {
                if (pathPts.length < 2) return Infinity;
                let md = Infinity;
                for (let k = 0; k < pathPts.length - 1; k++) {
                  const p1 = pathPts[k],
                    p2 = pathPts[k + 1];
                  const L2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
                  if (!L2) continue;
                  const tt =
                    ((tx - p1.x) * (p2.x - p1.x) +
                      (ty - p1.y) * (p2.y - p1.y)) /
                    L2;
                  const cl = Math.max(0, Math.min(1, tt));
                  const px = p1.x + cl * (p2.x - p1.x),
                    py = p1.y + cl * (p2.y - p1.y);
                  const d = Math.hypot(tx - px, ty - py);
                  if (d < md) md = d;
                }
                return md * scale;
              })();
              const distPassed = (function () {
                if (pathPts.length < 2 || tramSegIndex < 0) return Infinity;
                let md = Infinity;
                for (
                  let k = 0;
                  k <= Math.min(tramSegIndex, pathPts.length - 2);
                  k++
                ) {
                  const p1 = pathPts[k],
                    p2 = pathPts[k + 1];
                  const L2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
                  if (!L2) continue;
                  const tt =
                    ((tx - p1.x) * (p2.x - p1.x) +
                      (ty - p1.y) * (p2.y - p1.y)) /
                    L2;
                  const cl = Math.max(0, Math.min(1, tt));
                  const px = p1.x + cl * (p2.x - p1.x),
                    py = p1.y + cl * (p2.y - p1.y);
                  const d = Math.hypot(tx - px, ty - py);
                  if (d < md) md = d;
                }
                return md * scale;
              })();
              const distStation = (function () {
                if (!stationsForAngle.length) return Infinity;
                const DEFAULT_LABEL_OFFSET_X = 25,
                  DEFAULT_LABEL_OFFSET_Y = 0;
                let md = Infinity;
                for (const st of stationsForAngle) {
                  const offsetX =
                    st.offset_x === 0 && st.offset_y === 0
                      ? DEFAULT_LABEL_OFFSET_X
                      : st.offset_x || 0 * 3;
                  const offsetY =
                    st.offset_x === 0 && st.offset_y === 0
                      ? DEFAULT_LABEL_OFFSET_Y
                      : st.offset_y || 0 * 3;
                  const lx = st.longitude + offsetX,
                    ly = st.latitude + offsetY;
                  const d = Math.hypot(tx - lx, ty - ly);
                  if (d < md) md = d;
                }
                return md * scale;
              })();
              let weight = 0;
              if (distPath < minPath) weight += 100 * (1 - distPath / minPath);
              if (distPassed < minPassed)
                weight += 10 * (1 - distPassed / minPassed);
              if (distStation < minStation)
                weight += 1000 * (1 - distStation / minStation);
              if (weight < bestScore) {
                bestScore = weight;
                bestAng = ang;
              }
            }
            return bestAng;
          })();

          return (
            <TramIconWebGL
              x={screenX}
              y={screenY}
              optimalAngle={optimalAngle}
              scale={scale}
            />
          );
        })()}
      </div>
    </div>
  );
});

export default WebGLMap;
