"use client";

import { useEffect, useState, useRef } from "react";
import { useParams } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Compass } from "lucide-react";
import { PostgrestError } from "@supabase/supabase-js";
import * as THREE from 'three';
import React from "react";
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { Raycaster, Vector2, Vector3, Object3D, Mesh, ShapeGeometry, MeshBasicMaterial, Shape, Quaternion, BufferGeometry } from 'three';
import { useMinimap } from "../../contexts/MinimapContext";
import StopsMapModal from "../../components/StopsMapModal";
import { toast } from "@/components/ui/use-toast";
import GameStepsMap from '../../components/GameStepsMap';

// Helper functions
const handleEndGame = async (
  gameId: string,
  supabase: any,
  setEndGameScore: (score: number) => void,
  setEndGameDuration: (duration: string) => void,
  setShowEndGameModal: (show: boolean) => void
) => {
  try {
    const { data: endGameData, error: endGameError } = await supabase.functions.invoke('end-game', {
      body: { game_id: gameId },
    });

    if (endGameError) {
      console.error("Error calling end-game function:", endGameError.message);
      toast({
        title: "Error",
        description: "Failed to end game. Please try again.",
        variant: "destructive",
      });
    } else if (endGameData) {
      setEndGameScore(endGameData.score);
      setEndGameDuration(endGameData.duration);
      setShowEndGameModal(true);
    } else {
      toast({
        title: "Warning",
        description: "Game ended but no score was recorded.",
        variant: "destructive",
      });
    }
  } catch (e: any) {
    console.error("Exception calling end-game function:", e.message);
    toast({
      title: "Error",
      description: "An unexpected error occurred while ending the game.",
      variant: "destructive",
    });
  }
};

const checkAndHandleEndGame = async (
  currentPoint: { lat: number; lng: number },
  endPoint: { lat: number; lng: number },
  gameId: string,
  supabase: any,
  setEndGameScore: (score: number) => void,
  setEndGameDuration: (duration: string) => void,
  setShowEndGameModal: (show: boolean) => void
) => {
  const distance = haversineDistance(currentPoint, endPoint);

  if (distance < distanceThreshold) {
    await handleEndGame(gameId, supabase, setEndGameScore, setEndGameDuration, setShowEndGameModal);
  }
};

const handleNavigation = async (
  lat: number,
  lng: number,
  game: Game,
  supabase: any,
  setGame: React.Dispatch<React.SetStateAction<Game | null>>,
  updateGameCurrentPoint: (lat: number, lng: number) => Promise<void>,
  fetchStreetViewMetadata: (lat: number, lng: number) => Promise<void>,
  setShowStopsMap: (show: boolean) => void,
  setEndGameScore: (score: number) => void,
  setEndGameDuration: (duration: string) => void,
  setShowEndGameModal: (show: boolean) => void,
  cameraRef: React.MutableRefObject<THREE.PerspectiveCamera | null>,
  lastYawRef: React.MutableRefObject<number | null>,
  skipStepInsertion: boolean = false
) => {
  try {
    // Store the camera's current yaw before switching
    if (cameraRef.current) {
      const cameraDirection = new THREE.Vector3();
      cameraRef.current.getWorldDirection(cameraDirection);
      lastYawRef.current = Math.atan2(cameraDirection.x, -cameraDirection.z);
    }

    // Insert step in steps table only if not skipping
    if (!skipStepInsertion) {
      const { error: stepError } = await supabase
        .from("game_steps")
        .insert({
          game_id: game.id,
          start_lat: game.current_point.lat,
          start_lng: game.current_point.lng,
          end_lat: lat,
          end_lng: lng,
          step_type: -1
        });

      if (stepError) {
        console.error("Error inserting step:", stepError);
      }
    }

    // Update local game state immediately for minimap
    setGame(prev => prev ? { ...prev, current_point: { lat, lng } } : prev);
    
    // Update current_point in Supabase
    await updateGameCurrentPoint(lat, lng);

    // Fetch new metadata and update view
    await fetchStreetViewMetadata(lat, lng);
    
    // Close the stops map
    setShowStopsMap(false);

    // Check if end point is reached
    if (game && game.end_point) {
      await checkAndHandleEndGame(
        { lat, lng },
        game.end_point,
        game.id,
        supabase,
        setEndGameScore,
        setEndGameDuration,
        setShowEndGameModal
      );
    }
  } catch (error) {
    console.error("Error during navigation:", error);
    toast({
      title: "Error",
      description: error instanceof Error ? error.message : "Failed to navigate",
      variant: "destructive",
    });
  }
};

interface Game {
  id: string;
  user_id: string;
  city_id: string;
  start_point: { lat: number; lng: number };
  end_point: { lat: number; lng: number };
  current_point: { lat: number; lng: number };
  status: string;
}

type Stop = {
  stop_id: string;
  stop_name: string;
  lat: number;
  lng: number;
  routes: any[];
  distance_meters: number;
};


interface StreetViewMetadata {
  panoId: string;
  lat: number;
  lng: number;
  imageHeight: number;
  imageWidth: number;
  tileHeight: number;
  tileWidth: number;
  heading: number;
  tilt: number;
  roll: number;
  imageryType: string;
  date: string;
  copyright: string;
  reportProblemLink: string;
  addressComponents: Array<{
    longName: string;
    shortName: string;
    types: string[];
  }>;
  links: Array<{
    panoId: string;
    heading: number;
    text: string;
  }>;
}

interface PanoramaTile {
  x: number;
  y: number;
  data: Blob;
}

const distanceThreshold = 200;

// Helper function to calculate distance between two lat/lng points (Haversine formula)
const haversineDistance = (coords1: { lat: number; lng: number }, coords2: { lat: number; lng: number }) => {
  const toRad = (x: number) => (x * Math.PI) / 180;
  const R = 6371e3; // Earth radius in metres

  const dLat = toRad(coords2.lat - coords1.lat);
  const dLng = toRad(coords2.lng - coords1.lng);
  const lat1 = toRad(coords1.lat);
  const lat2 = toRad(coords2.lat);

  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.sin(dLng / 2) * Math.sin(dLng / 2) * Math.cos(lat1) * Math.cos(lat2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c; // in metres
};

// Helper function to format duration from seconds to MM:SS
const formatDuration = (totalSeconds: number) => {
  if (typeof totalSeconds !== 'number' || isNaN(totalSeconds)) {
    return "00:00"; // Or some other default/error indicator
  }
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};

const animateCameraToDirection = async (
  camera: THREE.PerspectiveCamera,
  controls: OrbitControls,
  arrowMesh: THREE.Mesh,
  streetViewMetadata: StreetViewMetadata,
  duration: number = 700
): Promise<void> => {
  return new Promise((resolve) => {
    const startTime = performance.now();
    const startPosition = camera.position.clone();

    // Store camera's yaw (to restore later)
    const currentDir = new THREE.Vector3();
    camera.getWorldDirection(currentDir);
    const currentYaw = Math.atan2(currentDir.x, -currentDir.z);

    // Get link.heading from parent pivot's userData
    const parentPivot = arrowMesh.parent as THREE.Object3D;
    const linkHeading = parentPivot?.userData?.heading ?? 0;

    // Calculate relative heading (same as used when creating arrow rotation)
    const relativeHeading = (streetViewMetadata.heading - linkHeading  - 90) % 360;
    const headingRad = THREE.MathUtils.degToRad(relativeHeading);

    // Compute forward direction from heading
    const moveDirection = new THREE.Vector3(
      Math.sin(headingRad),
      0,
      -Math.cos(headingRad)
    ).normalize();

    const moveDistance = 5;
    const targetPosition = startPosition.clone().add(moveDirection.clone().multiplyScalar(moveDistance));

    const animate = () => {
      const now = performance.now();
      const elapsed = now - startTime;
      const progress = Math.min(elapsed / duration, 1);

      const ease = progress < 0.5
        ? 2 * progress * progress
        : 1 - Math.pow(-2 * progress + 2, 2) / 2;

      camera.position.lerpVectors(startPosition, targetPosition, ease);

      // Always look in original yaw direction
      const lookVec = new THREE.Vector3(Math.sin(currentYaw), 0, -Math.cos(currentYaw));
      const lookAt = new THREE.Vector3().addVectors(camera.position, lookVec);
      camera.lookAt(lookAt);
      controls.target.copy(lookAt);
      controls.update();

      if (progress < 1) {
        requestAnimationFrame(animate);
      } else {
        // Reset camera to center but preserve yaw
        camera.position.set(0, 0, 0.01);
        const finalLookAt = new THREE.Vector3().addVectors(camera.position, lookVec);
        camera.lookAt(finalLookAt);
        controls.target.copy(finalLookAt);
        controls.update();
        resolve();
      }
    };

    animate();
  });
};



export default function GamePage() {
  const params = useParams();
  const { updateMinimapPosition, setShowMinimap, setEndPoint } = useMinimap();
  const [game, setGame] = useState<Game | null>(null);
  const [loading, setLoading] = useState(true);
  const [initialPanoramaReady, setInitialPanoramaReady] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [streetViewMetadata, setStreetViewMetadata] = useState<StreetViewMetadata | null>(null);
  const [currentStreetViewMetadata, setCurrentStreetViewMetadata] = useState<StreetViewMetadata | null>(null);
  const [sessionToken, setSessionToken] = useState<string | null>(null);
  const [panoramaTiles, setPanoramaTiles] = useState<PanoramaTile[]>([]);
  const [panoId, setPanoId] = useState<string | null>(null);
  const panoramaContainerRef = useRef<HTMLDivElement>(null);
  const supabase = createClient();

  // State for end game modal
  const [showEndGameModal, setShowEndGameModal] = useState(false);
  const [endGameScore, setEndGameScore] = useState<number | null>(null);
  const [endGameDuration, setEndGameDuration] = useState<string | null>(null); // Store as formatted string

  // Add new state for give up modal
  const [showGiveUpModal, setShowGiveUpModal] = useState(false);
  const [finalDistance, setFinalDistance] = useState<number | null>(null);

  // State for displayed public transport routes
  const [displayedRoutes, setDisplayedRoutes] = useState<any[]>([]);
  const [hasAttemptedStopFetch, setHasAttemptedStopFetch] = useState(false);

  // State for scene readiness
  const [isThreeJsSceneReady, setIsThreeJsSceneReady] = useState(false);

  // Refs for Three.js objects
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
  const sceneRef = useRef<THREE.Scene | null>(null);
  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
  const controlsRef = useRef<OrbitControls | null>(null);
  const sphereGeometryRef = useRef<THREE.SphereGeometry | null>(null);
  const sphereMaterialRef = useRef<THREE.MeshBasicMaterial | null>(null);
  const sphereRef = useRef<THREE.Mesh | null>(null);

  // Refs for Three.js objects related to arrows
  const sceneArrowsContainerRef = useRef<THREE.Object3D | null>(null);
  const raycasterRef = useRef(new THREE.Raycaster());
  const mouseRef = useRef(new THREE.Vector2());
  const arrowShapeGeometryRef = useRef<THREE.BufferGeometry | null>(null);

  // Store the last yaw direction globally in the component
  const lastYawRef = useRef<number | null>(null);

  const [currentYaw, setCurrentYaw] = useState<number>(THREE.MathUtils.degToRad(90));

  const [selectedRoute, setSelectedRoute] = useState<string | null>(null);
  const [routeStops, setRouteStops] = useState<Stop[]>([]);
  const [showStopsMap, setShowStopsMap] = useState(false);

  // Add state for directions
  const [routeDirections, setRouteDirections] = useState<any[]>([]);

  // Add these at the top level of the component, with other state declarations
  const [prefetchedTiles] = useState<Map<string, PanoramaTile[]>>(new Map());
  const [prefetchedMetadata] = useState<Map<string, StreetViewMetadata>>(new Map());

  const getSessionToken = async () => {
    try {
      const response = await fetch(`https://tile.googleapis.com/v1/createSession?key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          mapType: 'streetview',
          language: 'en-US',
          region: 'US'
        })
      });
      
      const data = await response.json();
      if (data.session) {
        setSessionToken(data.session);
        // Store the session token in localStorage with expiration
        const expirationDate = new Date();
        expirationDate.setDate(expirationDate.getDate() + 14); // 14 days from now
        localStorage.setItem('streetViewSession', JSON.stringify({
          token: data.session,
          expiresAt: expirationDate.toISOString()
        }));
      }
    } catch (err) {
    }
  };

  async function getNearbyStops(lat: number, lng: number) {
    const { data: stops, error } = await supabase.rpc("get_nearby_stops", {
      p_lat: lat,
      p_lng: lng,
      p_radius_meters: 20, 
    });
    setHasAttemptedStopFetch(true); // Indicate that a fetch attempt has been made

    if (error) {
      console.error("Error fetching nearby stops:", error.message);
      return [];
    }
    if (stops && stops.length > 0) {
      let uniqueRoutes: any[] = [];
      const numberRoutes: any[] = []
      const letterRoutes: any[] = []
      for (const stop of stops) {
        if (stop.routes) { // Ensure stop.routes is not null or undefined
          for (const route of stop.routes) {
            if (!uniqueRoutes.includes(route.route_short_name) && route.route_short_name !== '' && route.route_short_name[0] !== 'N') {
              uniqueRoutes.push(route.route_short_name);
              if(route.route_short_name[0] == 'M'){
                letterRoutes.push({
                  ...route,
                  stop_id: stop.stop_id
                })
              } else {
                numberRoutes.push({
                  ...route,
                  stop_id: stop.stop_id
                })
              }
            }
          }
        }
      }
      numberRoutes.sort((a, b) => Number(a.route_short_name) - Number(b.route_short_name))
      letterRoutes.sort((a, b) => a.route_short_name.localeCompare(b.route_short_name))
      uniqueRoutes = [...letterRoutes, ...numberRoutes]
      setDisplayedRoutes(uniqueRoutes);
    } else {
      setDisplayedRoutes([]); // Clear routes if no stops or no routes found
    }
    return stops;
  }

  async function getStopsByRoute(route: any) {
    const { data: stops, error } = await supabase
    .rpc("get_stops_by_route", { p_route_id: route.route_id });

    const { data: directions, error: directionsError } = await supabase
    .from("route_stop_orders").select("*").eq("route_id", route.route_id);

    if(directionsError){
      console.error("Error fetching directions:", directionsError.message);
    }
    
    if (error) {
      console.error("Error fetching stops by route:", error.message);
      return [];
    }
    // const uniqueStops = getUniqueStopsByName(stops);
    const uniqueStops = stops.map((el: any) => {
      return {
        ...el,
        current_stop: el.stop_id == route.stop_id
      }
    });
    setRouteStops(uniqueStops);
    setRouteDirections(directions || []);
    setSelectedRoute(route.route_short_name);
    setShowStopsMap(true);
    return uniqueStops;
  }

  async function getLinks(lat: number, lng: number, currentLink: any) {
    const linkMetadata = await getMetadata(currentLink.panoId)
    const diffLat = lat - linkMetadata.lat
    const diffLng = lng - linkMetadata.lng
    let res = await getPanoIds(lat + diffLat, lng + diffLng)
    if(res.length > 0){
      return res[0]
    }else return ""
  }

  async function getMetadata(panoId: string) {
    const storedSession = localStorage.getItem('streetViewSession');
    let currentSession = sessionToken;
    
    if (!currentSession && storedSession) {
      const { token, expiresAt } = JSON.parse(storedSession);
      if (new Date(expiresAt) > new Date()) {
        currentSession = token;
        setSessionToken(token);
      }
    }

    if (!currentSession) {
      await getSessionToken();
      currentSession = sessionToken;
    }

    if (!currentSession) {
      throw new Error('Failed to get session token');
    }

    const metadataResponse = await fetch(
      `https://tile.googleapis.com/v1/streetview/metadata?session=${currentSession}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&panoId=${panoId}`
    );
    const metadata = await metadataResponse.json();
    console.log(metadata)
    return metadata;

  }

  async function getPanoIds(lat: number, lng: number) {
    const storedSession = localStorage.getItem('streetViewSession');
    let currentSession = sessionToken;
    
    if (!currentSession && storedSession) {
      const { token, expiresAt } = JSON.parse(storedSession);
      if (new Date(expiresAt) > new Date()) {
        currentSession = token;
        setSessionToken(token);
      }
    }

    if (!currentSession) {
      await getSessionToken();
      currentSession = sessionToken;
    }

    if (!currentSession) {
      throw new Error('Failed to get session token');
    }
    const body = {
      locations: [{
        lat: lat,
        lng: lng,
      }],
      radius: 5
    }
    // First, get the panoId
    const panoResponse = await fetch(
      `https://tile.googleapis.com/v1/streetview/panoIds?session=${currentSession}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body)
      }
    );
    const panoData = await panoResponse.json();
    return panoData.panoIds;
  }

  function getUniqueStopsByName(stops: any[]) {
    const seen = new Map();
    for (const stop of stops) {
      if (!seen.has(stop.stop_name)) {
        seen.set(stop.stop_name, stop);
      }
    }
    return Array.from(seen.values());
  }
  

  // Add this new function after the existing helper functions
  const applyPrefetchedTiles = (panoId: string) => {
    const tiles = prefetchedTiles.get(panoId);
    if (tiles) {
      setPanoramaTiles(tiles);
      return true;
    }
    return false;
  };

  // Add this new function after the existing helper functions
  const applyPrefetchedMetadata = (panoId: string) => {
    const metadata = prefetchedMetadata.get(panoId);
    if (metadata) {
      setStreetViewMetadata(metadata);
      return true;
    }
    return false;
  };

  // Modify preFetchLowResTiles to also store metadata
  const preFetchLowResTiles = async (panoId: string) => {
    console.log("preFetchLowResTiles", panoId)
    const zoom = 1;
    const tilesX = Math.pow(2, zoom);  // 2 tiles horizontally
    const tilesY = Math.pow(2, zoom - 1);  // 1 tile vertically
    const storedSession = localStorage.getItem('streetViewSession');
    let currentSession = sessionToken;
    
    if (!currentSession && storedSession) {
      const { token, expiresAt } = JSON.parse(storedSession);
      if (new Date(expiresAt) > new Date()) {
        currentSession = token;
        setSessionToken(token);
      }
    }

    if (!currentSession) {
      await getSessionToken();
      currentSession = sessionToken;
    }

    if (!currentSession) {
      throw new Error('Failed to get session token');
    }

    try {
      // First fetch metadata if not already stored
      if (!prefetchedMetadata.has(panoId)) {
        const metadataResponse = await fetch(
          `https://tile.googleapis.com/v1/streetview/metadata?session=${currentSession}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&panoId=${panoId}`
        );
        const metadata = await metadataResponse.json();
        if (metadata.error) throw new Error(metadata.error.message);
        
        // Handle single link case
        if(metadata.links && metadata.links.length == 1){
          const extraLinks = await getLinks(metadata.lat, metadata.lng, metadata.links[0])
          if(extraLinks != ""){
            metadata.links.push({
              panoId: extraLinks,
              heading: (metadata.links[0].heading - 180) % 360,
              text: "Additional link"
            })
          }
        }
        
        prefetchedMetadata.set(panoId, metadata);
      }

      const tilePromises = [];
      for (let x = 0; x < tilesX; x++) {
        for (let y = 0; y < tilesY; y++) {
          const url = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&x=${x}&y=${y}&zoom=${zoom}&nbt=1&fover=2`;
          tilePromises.push(
            fetch(url)
              .then(response => {
                if (!response.ok) {
                  throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.blob();
              })
              .then(blob => ({
                x,
                y,
                data: blob
              }))
              .catch(error => {
                console.error(`[Pre-fetch] Error fetching tile at x:${x}, y:${y}:`, error);
                return null;
              })
          );
        }
      }
      const results = await Promise.all(tilePromises);
      const validResults = results.filter((tile): tile is PanoramaTile => tile !== null);
      prefetchedTiles.set(panoId, validResults);
    } catch (err) {
      console.error('[Pre-fetch] Error in preFetchLowResTiles:', err);
    }
  };

  // Modify runHeavyNavigationLogic to use prefetched metadata
  const runHeavyNavigationLogic = async (
    newPanoId: string,
    game: Game,
    supabase: any,
    setGame: React.Dispatch<React.SetStateAction<Game | null>>,
    updateGameCurrentPoint: (lat: number, lng: number) => Promise<void>,
    fetchStreetViewMetadata: (lat: number, lng: number) => Promise<void>,
    setShowStopsMap: (show: boolean) => void,
    setEndGameScore: (score: number) => void,
    setEndGameDuration: (duration: string) => void,
    setShowEndGameModal: (show: boolean) => void,
    cameraRef: React.MutableRefObject<THREE.PerspectiveCamera | null>,
    lastYawRef: React.MutableRefObject<number | null>
  ) => {
    try {
      // Try to get metadata from prefetched data first
      let newMetadata = prefetchedMetadata.get(newPanoId);
      
      // If not found, fetch it
      if (!newMetadata) {
        const metadataResponse = await fetch(
          `https://tile.googleapis.com/v1/streetview/metadata?session=${sessionToken}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&panoId=${newPanoId}`
        );
        const responseData = await metadataResponse.json();
        
        // Check if response has error
        if ('error' in responseData) {
          throw new Error(responseData.error.message);
        }
        
        // Type assertion since we know it's StreetViewMetadata if no error
        newMetadata = responseData as StreetViewMetadata;
        
        // Handle single link case
        if(newMetadata.links && newMetadata.links.length === 1){
          const extraLinks = await getLinks(newMetadata.lat, newMetadata.lng, newMetadata.links[0]);
          if(extraLinks !== ""){
            newMetadata.links.push({
              panoId: extraLinks,
              heading: (newMetadata.links[0].heading - 180) % 360,
              text: "Additional link"
            });
          }
        }
        
        // Store for future use
        prefetchedMetadata.set(newPanoId, newMetadata);
      }

      if (!newMetadata) {
        throw new Error('Failed to get metadata');
      }
      await getNearbyStops(newMetadata.lat, newMetadata.lng);
      // Fetch high-res tiles before handling navigation
      const highResZoom = 3;
      const tilesX = Math.pow(2, highResZoom);
      const tilesY = Math.pow(2, highResZoom - 1);
      const highResTilePromises = [];
      
      for (let x = 0; x < tilesX; x++) {
        for (let y = 0; y < tilesY; y++) {
          const url = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${newPanoId}&output=tile&x=${x}&y=${y}&zoom=${highResZoom}&nbt=1&fover=2`;
          highResTilePromises.push(
            fetch(url)
              .then(response => {
                if (!response.ok) {
                  throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.blob();
              })
              .then(blob => ({
                x,
                y,
                data: blob
              }))
              .catch(error => {
                console.error(`[Navigation] Error fetching high-res tile at x:${x}, y:${y}:`, error);
                throw error;
              })
          );
        }
      }

      // Wait for high-res tiles and apply them
      const highResResults = await Promise.all(highResTilePromises);

      // Combine the updates into a single state update
      setCurrentStreetViewMetadata(newMetadata);
      setPanoramaTiles(highResResults);
      console.log("new metadata set");

      await handleNavigation(
        newMetadata.lat,
        newMetadata.lng,
        game,
        supabase,
        setGame,
        updateGameCurrentPoint,
        fetchStreetViewMetadata,
        setShowStopsMap,
        setEndGameScore,
        setEndGameDuration,
        setShowEndGameModal,
        cameraRef,
        lastYawRef
      );
    } catch (err) {
      console.error("[Navigation] Error in heavy navigation logic:", err);
    }
  };

  const fetchPanoramaTiles = async (panoId: string, isInitialLoad: boolean = false) => {
    // Only fetch tiles if this is the initial load
    if (!isInitialLoad) {
      return;
    }
    
    console.log("fetchPanoramaTiles - initial load", panoId)
    
    // First fetch zoom level 1 tiles (low-res)
    const initialZoom = 1;
    const tilesX = Math.pow(2, initialZoom);  // 2 tiles horizontally
    const tilesY = Math.pow(2, initialZoom - 1);  // 1 tile vertically

    try {
      // Fetch initial low-res tiles
      const initialTilePromises = [];
      for (let x = 0; x < tilesX; x++) {
        for (let y = 0; y < tilesY; y++) {
          const url = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&x=${x}&y=${y}&zoom=${initialZoom}&nbt=1&fover=2`;
          initialTilePromises.push(
            fetch(url)
              .then(response => {
                if (!response.ok) {
                  throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.blob();
              })
              .then(blob => {
                return {
                  x,
                  y,
                  data: blob
                };
              })
              .catch(error => {
                console.error(`[Panorama] Error fetching initial tile at x:${x}, y:${y}:`, error);
                throw error;
              })
          );
        }
      }

      // Wait for initial tiles to be fetched and apply them immediately
      const initialResults = await Promise.all(initialTilePromises);
      setPanoramaTiles(initialResults);
      setInitialPanoramaReady(true); // Set initial panorama as ready

      // Then fetch high-res tiles (zoom level 3) in the background
      const highResZoom = 3;
      const highResTilesX = Math.pow(2, highResZoom);  // 8 tiles horizontally
      const highResTilesY = Math.pow(2, highResZoom - 1);  // 4 tiles vertically

      const highResTilePromises = [];
      for (let x = 0; x < highResTilesX; x++) {
        for (let y = 0; y < highResTilesY; y++) {
          const url = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&x=${x}&y=${y}&zoom=${highResZoom}&nbt=1&fover=2`;
          highResTilePromises.push(
            fetch(url)
              .then(response => {
                if (!response.ok) {
                  throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.blob();
              })
              .then(blob => {
                return {
                  x,
                  y,
                  data: blob
                };
              })
              .catch(error => {
                console.error(`[Panorama] Error fetching high-res tile at x:${x}, y:${y}:`, error);
                throw error;
              })
          );
        }
      }

      // Wait for high-res tiles to be fetched and apply them
      const highResResults = await Promise.all(highResTilePromises);
      setPanoramaTiles(highResResults);
    } catch (err) {
      console.error('[Panorama] Error in fetchPanoramaTiles:', err);
    }
  };

  const fetchStreetViewMetadata = async (lat: number, lng: number, isInitialLoad: boolean = false) => {
    try {
      // Check if we have a valid session token
      const storedSession = localStorage.getItem('streetViewSession');
      let currentSession = sessionToken;
      
      if (!currentSession && storedSession) {
        const { token, expiresAt } = JSON.parse(storedSession);
        if (new Date(expiresAt) > new Date()) {
          currentSession = token;
          setSessionToken(token);
        }
      }

      if (!currentSession) {
        await getSessionToken();
        currentSession = sessionToken;
      }

      if (!currentSession) {
        throw new Error('Failed to get session token');
      }
      const body = {
        locations: [{
          lat: lat,
          lng: lng,
        }],
        radius: 100
      }
      // First, get the panoId
      const panoResponse = await fetch(
        `https://tile.googleapis.com/v1/streetview/panoIds?session=${currentSession}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(body)
        }
      );
      const panoData = await panoResponse.json();
      if (panoData.panoIds.length > 0) {
        const metadataResponse = await fetch(
          `https://tile.googleapis.com/v1/streetview/metadata?session=${currentSession}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&panoId=${panoData.panoIds[0]}`
        );
        const metadata = await metadataResponse.json();
        if(metadata.links && metadata.links.length == 1){
          const extraLinks = await getLinks(metadata.lat, metadata.lng, metadata.links[0])
          if(extraLinks != ""){
            metadata.links.push({
              panoId: extraLinks,
              heading: (metadata.links[0].heading - 180) % 360,
              text: "Additional link"
            })
          }
        }

        // Pre-fetch low-res tiles for all navigation links
        if (metadata.links) {
          metadata.links.forEach((link: { panoId: string }) => {
            if (link.panoId) {
              preFetchLowResTiles(link.panoId);
            }
          });
        }

        const nearbyStops = await getNearbyStops(metadata.lat, metadata.lng);
        
        // Fetch initial tiles before setting metadata
        await fetchPanoramaTiles(panoData.panoIds[0], isInitialLoad);
        
        // Only set metadata after tiles are loaded
        setStreetViewMetadata(metadata);
      }
    } catch (err) {
      console.error('[Metadata] Error fetching Street View metadata:', err);
    }
  };

  useEffect(() => {
    async function fetchGame() {
      try {
        const { data, error } = await supabase
          .from("games")
          .select(`
            *
          `)
          .eq("id", params.id)
          .single();

        if (error) throw error;
        setGame(data);
        if (data?.current_point) {
          await fetchStreetViewMetadata(data.current_point.lat, data.current_point.lng, true);
        }
        if (data?.end_point) {
          setEndPoint(data.end_point.lat, data.end_point.lng);
        }
      } catch (err) {
        const error = err as PostgrestError;
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }

    fetchGame();
  }, [params.id, supabase, setEndPoint]);

  // Minimal Three.js implementation - Main Setup Effect
  useEffect(() => {
    if (loading || error || !game || !panoramaContainerRef.current) {
      // If loading, error, no game, or container not ready, do nothing or cleanup
      if (rendererRef.current && panoramaContainerRef.current) {
        // Attempt to clean up if game becomes invalid after initialization
        const canvas = panoramaContainerRef.current.querySelector('canvas');
        if (canvas) canvas.remove(); // Remove canvas created by this renderer
      }
      return;
    }

    // Initialize Three.js core components only if they haven't been already
    if (!rendererRef.current) {
      const width = panoramaContainerRef.current.clientWidth || 800;
      const height = panoramaContainerRef.current.clientHeight || 600;

      if (width === 0 || height === 0) {
        return; // Wait for valid dimensions
      }

      // Scene
      sceneRef.current = new THREE.Scene();

      // Camera
      cameraRef.current = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
      cameraRef.current.position.set(0, 0, 0.01);
      // Initial lookAt will be handled by controls or specific yaw update effect

      // Renderer
      rendererRef.current = new THREE.WebGLRenderer({ antialias: true });
      rendererRef.current.setSize(width, height);
      rendererRef.current.setClearColor(0x000000);
      panoramaContainerRef.current.appendChild(rendererRef.current.domElement);
      rendererRef.current.autoClear = false;

      // Sphere Geometry & Material (created once)
      sphereGeometryRef.current = new THREE.SphereGeometry(50, 60, 40);
      sphereMaterialRef.current = new THREE.MeshBasicMaterial({ side: THREE.BackSide, wireframe: true, color: 0x00ff00 });
      sphereRef.current = new THREE.Mesh(sphereGeometryRef.current, sphereMaterialRef.current);
      sceneRef.current.add(sphereRef.current);
      
      // OrbitControls
      controlsRef.current = new OrbitControls(cameraRef.current, rendererRef.current.domElement);
      controlsRef.current.enablePan = false;
      controlsRef.current.enableZoom = false;
      controlsRef.current.enableDamping = true;
      controlsRef.current.dampingFactor = 0.05;
      controlsRef.current.rotateSpeed = -0.25;
      // Initial target for controls will be set based on yaw

      // Initialize Arrow Container in Scene (if not already)
      if (!sceneArrowsContainerRef.current) {
        sceneArrowsContainerRef.current = new THREE.Object3D();
        if (sceneRef.current) { // Ensure sceneRef is also available
          sceneRef.current.add(sceneArrowsContainerRef.current);
        } else {
        }
      }
      setIsThreeJsSceneReady(true); // Signal that the core scene setup is complete
    }
    
    // Update camera lookAt based on lastYawRef or default
    // This part might need to be adjusted or moved if yaw changes frequently outside of initial load
    if (cameraRef.current && controlsRef.current) {
        let yaw = lastYawRef.current !== null ? lastYawRef.current : THREE.MathUtils.degToRad(90);
        const forward = new THREE.Vector3(Math.sin(yaw), 0, -Math.cos(yaw));
        cameraRef.current.lookAt(forward);
        controlsRef.current.target.copy(forward);
        controlsRef.current.update(); // Ensure controls reflect this change
    }


    // Animation loop
    let animationFrameId: number;
    const cameraWorldPositionVec = new THREE.Vector3();
    const cameraDirectionForYaw = new THREE.Vector3();
    let prevYaw = THREE.MathUtils.degToRad(90);

    const animate = () => {
      animationFrameId = requestAnimationFrame(animate);
      if (!rendererRef.current || !sceneRef.current || !cameraRef.current || !controlsRef.current) return;

      controlsRef.current.update();

      if (cameraRef.current && sceneArrowsContainerRef.current) {
        cameraRef.current.getWorldDirection(cameraDirectionForYaw);
        cameraRef.current.getWorldPosition(cameraWorldPositionVec);

        const newYaw = Math.atan2(cameraDirectionForYaw.x, -cameraDirectionForYaw.z);
        // Update currentYaw during panning by checking if yaw has changed significantly
        if (controlsRef.current.enabled && Math.abs(prevYaw - newYaw) > 0.001) {
          setCurrentYaw(newYaw);
          prevYaw = newYaw;
        }

        const hudRadius = 25;
        const hudYOffset = -10;
        const targetHudPosition = cameraDirectionForYaw.clone().multiplyScalar(hudRadius).add(cameraWorldPositionVec);
        targetHudPosition.y = cameraWorldPositionVec.y + hudYOffset;
        sceneArrowsContainerRef.current.position.copy(targetHudPosition);
      }
      
      rendererRef.current.clear();
      rendererRef.current.render(sceneRef.current, cameraRef.current);
      rendererRef.current.clearDepth();
      if (sceneArrowsContainerRef.current && Object.keys(sceneArrowsContainerRef.current).length > 0 && sceneArrowsContainerRef.current.children.length > 0) { // check if it's not an empty object
        rendererRef.current.render(sceneArrowsContainerRef.current, cameraRef.current);
      }
    };
    animate(); // Start animation loop

    // Cleanup for the main setup effect (runs on component unmount)
    return () => {
      cancelAnimationFrame(animationFrameId);
      setIsThreeJsSceneReady(false); // Reset scene readiness flag

      if (controlsRef.current) {
        controlsRef.current.dispose();
        controlsRef.current = null;
      }
      // Dispose of scene specific arrows container children
      if (sceneArrowsContainerRef.current) {
         if(sceneRef.current) sceneRef.current.remove(sceneArrowsContainerRef.current); 
         while (sceneArrowsContainerRef.current.children.length > 0) {
            const child = sceneArrowsContainerRef.current.children[0];
            sceneArrowsContainerRef.current.remove(child);
            if ((child as THREE.Mesh).isMesh) {
                const mesh = child as THREE.Mesh;
                if (mesh.geometry) mesh.geometry.dispose();
                if (mesh.material) {
                    if (Array.isArray(mesh.material)) {
                        mesh.material.forEach(m => m.dispose());
                    } else {
                        mesh.material.dispose();
                    }
                }
            }
         }
        // sceneArrowsContainerRef.current = null; // Keep the container itself, just clear its children
      }

      if (sphereRef.current && sceneRef.current) {
        sceneRef.current.remove(sphereRef.current);
      }
      if (sphereMaterialRef.current) {
        if (sphereMaterialRef.current.map) sphereMaterialRef.current.map.dispose();
        sphereMaterialRef.current.dispose();
        sphereMaterialRef.current = null;
      }
      if (sphereGeometryRef.current) {
        sphereGeometryRef.current.dispose();
        sphereGeometryRef.current = null;
      }
      sphereRef.current = null;

      if (rendererRef.current) {
        // Remove canvas from DOM
        if (panoramaContainerRef.current && rendererRef.current.domElement.parentNode === panoramaContainerRef.current) {
            panoramaContainerRef.current.removeChild(rendererRef.current.domElement);
        }
        rendererRef.current.dispose();
        rendererRef.current = null;
      }
      if (sceneRef.current) {
        // sceneRef.current.dispose(); // Scene doesn't have a dispose method directly
        sceneRef.current = null;
      }
      if (cameraRef.current) {
        cameraRef.current = null;
      }
      // Ensure arrowShapeGeometry is cleaned up by its own effect or globally
    };
  // Dependencies: Only re-run if loading/error state changes, or game ID changes (forcing a full reset).
  // Crucially, NOT dependent on panoramaTiles or full streetViewMetadata object.
  }, [loading, error, game?.id]); // game?.id ensures it runs once game is loaded.

  // Effect for Panorama Texture Update
  useEffect(() => {
    if (!sphereMaterialRef.current || panoramaTiles.length === 0 || !streetViewMetadata || !streetViewMetadata.tileWidth || !streetViewMetadata.tileHeight) {
      if (sphereMaterialRef.current) {
        if (sphereMaterialRef.current.map) sphereMaterialRef.current.map.dispose();
        sphereMaterialRef.current.map = null;
        sphereMaterialRef.current.wireframe = true;
        sphereMaterialRef.current.color.set(0x00ff00); // Green wireframe
        sphereMaterialRef.current.needsUpdate = true;
      }
      return;
    }

    // Skip if we already have the same number of tiles
    if (sphereMaterialRef.current.map && panoramaTiles.length === (sphereMaterialRef.current.map as any).userData?.tileCount) {
      return;
    }

    const { tileWidth, tileHeight } = streetViewMetadata;
    const zoom = panoramaTiles.length <= 2 ? 1 : 3; // Determine zoom level based on number of tiles
    
    const tilesX = Math.pow(2, zoom);
    const tilesY = Math.pow(2, zoom - 1);
    const fullWidth = tilesX * tileWidth;
    const fullHeight = tilesY * tileHeight;
    const offscreenCanvas = document.createElement('canvas');
    offscreenCanvas.width = fullWidth;
    offscreenCanvas.height = fullHeight;
    const ctx = offscreenCanvas.getContext('2d');

    if (ctx) {
      const imageLoadPromises = panoramaTiles.map(tile =>
        createImageBitmap(tile.data).then(imageBitmap => {
          ctx.drawImage(imageBitmap, tile.x * tileWidth, tile.y * tileHeight);
          imageBitmap.close();
        })
      );
      Promise.all(imageLoadPromises).then(() => {
        // console.log("texture update", panoramaTiles.length)
        if (sphereMaterialRef.current) {
          if (sphereMaterialRef.current.map) sphereMaterialRef.current.map.dispose();
          const texture = new THREE.CanvasTexture(offscreenCanvas);
          texture.wrapS = THREE.RepeatWrapping;
          texture.repeat.x = -1;
          // if (streetViewMetadata && streetViewMetadata.heading !== undefined) {
          //   const rotation = THREE.MathUtils.degToRad(streetViewMetadata.heading);
          //   texture.offset.x = rotation / (2 * Math.PI);
          // }
          texture.needsUpdate = true;
          // Store the tile count in the texture's userData
          (texture as any).userData = { tileCount: panoramaTiles.length };
          sphereMaterialRef.current.map = texture;
          sphereMaterialRef.current.wireframe = false;
          sphereMaterialRef.current.color.set(0xffffff); // White color when textured
          sphereMaterialRef.current.needsUpdate = true;
        }
      }).catch(err => {
        console.error('[Texture] Error creating texture:', err);
        if (sphereMaterialRef.current) {
          sphereMaterialRef.current.map = null;
          sphereMaterialRef.current.wireframe = true;
          sphereMaterialRef.current.color.set(0x00ff00);
          sphereMaterialRef.current.needsUpdate = true;
        }
      });
    } else {
      console.error('[Texture] Failed to get canvas context');
      if (sphereMaterialRef.current) {
        sphereMaterialRef.current.map = null;
        sphereMaterialRef.current.wireframe = true;
        sphereMaterialRef.current.color.set(0x00ff00);
        sphereMaterialRef.current.needsUpdate = true;
      }
    }
  }, [panoramaTiles, streetViewMetadata, sphereMaterialRef]);

  // Effect for Navigation Arrows Update
  useEffect(() => {
    if (!arrowShapeGeometryRef.current) { // Create arrow geometry once if it doesn't exist
      const arrowShape = new THREE.Shape();
      arrowShape.moveTo(-0.4, -0.2); // Increased width for wider angle
      arrowShape.lineTo(0, 0.5);
      arrowShape.lineTo(0.4, -0.2); // Increased width for wider angle
      arrowShape.lineTo(0.3, -0.2); // Adjusted width
      arrowShape.lineTo(0, 0.2);
      arrowShape.lineTo(-0.3, -0.2); // Adjusted width
      arrowShape.lineTo(-0.4, -0.2); // Increased width for wider angle
      arrowShapeGeometryRef.current = new THREE.ExtrudeGeometry(arrowShape, { depth: 0.1, bevelEnabled: false });
      arrowShapeGeometryRef.current.center();
    }

    if (!isThreeJsSceneReady || !sceneArrowsContainerRef.current || !streetViewMetadata?.links || !arrowShapeGeometryRef.current) {
        // Clear previous arrows if scene not ready or no links or container
        if (sceneArrowsContainerRef.current) {
            while (sceneArrowsContainerRef.current.children.length > 0) {
                const arrowPivot = sceneArrowsContainerRef.current.children[0] as THREE.Object3D;
                sceneArrowsContainerRef.current.remove(arrowPivot);
                // Dispose materials of meshes within the pivot
                arrowPivot.traverse((object) => {
                    if ((object as THREE.Mesh).isMesh) {
                        const material = (object as THREE.Mesh).material;
                        if (material) {
                            if (Array.isArray(material)) {
                                material.forEach(m => m.dispose());
                            } else {
                                material.dispose(); // Dispose unique material
                            }
                        }
                    }
                });
            }
        }
      return;
    }
    
    // Clear previous arrows from the scene container
    if (sceneArrowsContainerRef.current) {
        while (sceneArrowsContainerRef.current.children.length > 0) {
            const arrowPivot = sceneArrowsContainerRef.current.children[0] as THREE.Object3D;
            sceneArrowsContainerRef.current.remove(arrowPivot);
            arrowPivot.traverse((object) => {
                if ((object as THREE.Mesh).isMesh) {
                    const material = (object as THREE.Mesh).material;
                    if (material) {
                        if (Array.isArray(material)) {
                            material.forEach(m => m.dispose());
                        } else {
                            material.dispose();
                        }
                    }
                }
            });
        }
    }

    const arrowTargetWorldHeight = 2;
    const shapeOriginalHeight = 1;
    const arrowScale = arrowTargetWorldHeight / shapeOriginalHeight;

    streetViewMetadata.links.forEach((link) => {
      const compassRadius = 3;
      const arrowMaterial = new THREE.MeshBasicMaterial({
        color: 0xffffff,
        side: THREE.DoubleSide,
        transparent: true,
        opacity: 0.8,
        depthTest: true,
        depthWrite: true
      });
      const arrowMesh = new THREE.Mesh(arrowShapeGeometryRef.current!, arrowMaterial);
      arrowMesh.scale.set(arrowScale, arrowScale, arrowScale);
      arrowMesh.position.set(0, 0, -compassRadius);
      arrowMesh.rotation.x = -Math.PI / 2;
      
      // Add hover interaction
      arrowMesh.userData = {
        type: 'navigation_arrow_mesh',
        panoId: link.panoId,
        heading: (streetViewMetadata.heading - link.heading + 90) % 360,
        text: link.text || '',
        originalColor: 0xffffff,
        hoverColor: 0x666666 // Darker color for hover
      };
      
      const arrowPivot = new THREE.Object3D();
      arrowPivot.rotation.y = THREE.MathUtils.degToRad((streetViewMetadata.heading - link.heading - 90) % 360);
      arrowPivot.userData = {
        type: 'navigation_arrow_pivot',
        panoId: link.panoId,
        heading: (streetViewMetadata.heading - link.heading + 90) % 360,
        text: link.text || ''
      };
      
      arrowPivot.add(arrowMesh);
      if (sceneArrowsContainerRef.current) {
        sceneArrowsContainerRef.current.add(arrowPivot);
      }
    });

  }, [isThreeJsSceneReady, streetViewMetadata?.links, streetViewMetadata?.heading, arrowShapeGeometryRef]);

  // Add hover effect handling
  useEffect(() => {
    const canvas = panoramaContainerRef.current?.querySelector('canvas');
    const currentCamera = cameraRef.current;
    if (!canvas || !currentCamera || !sceneArrowsContainerRef.current) {
        return;
    }

    let hoveredMesh: THREE.Mesh | null = null;

    const handleMouseMove = (event: MouseEvent) => {
        if (!sceneArrowsContainerRef.current || sceneArrowsContainerRef.current.children.length === 0) {
            return;
        }

        const rect = canvas.getBoundingClientRect();
        mouseRef.current.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
        mouseRef.current.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

        raycasterRef.current.setFromCamera(mouseRef.current, currentCamera);
        
        const meshesToIntersect: THREE.Mesh[] = [];
        sceneArrowsContainerRef.current.traverse((child) => {
            if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).userData.type === 'navigation_arrow_mesh') {
                meshesToIntersect.push(child as THREE.Mesh);
            }
        });

        const intersects = raycasterRef.current.intersectObjects(meshesToIntersect);

        // Reset previous hover state
        if (hoveredMesh && hoveredMesh.material instanceof THREE.MeshBasicMaterial) {
            hoveredMesh.material.color.setHex(hoveredMesh.userData.originalColor);
        }

        // Set new hover state
        if (intersects.length > 0) {
            hoveredMesh = intersects[0].object as THREE.Mesh;
            if (hoveredMesh.material instanceof THREE.MeshBasicMaterial) {
                hoveredMesh.material.color.setHex(hoveredMesh.userData.hoverColor);
            }
        } else {
            hoveredMesh = null;
        }
    };

    canvas.addEventListener('mousemove', handleMouseMove);
    return () => {
        canvas.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  // useEffect for click listener on canvas
  useEffect(() => {
    const canvas = panoramaContainerRef.current?.querySelector('canvas');
    const currentCamera = cameraRef.current;
    if (!canvas || !currentCamera || !sceneArrowsContainerRef.current) { // Check scene container
        return;
    }

    const handleClick = async (event: MouseEvent) => {
        if (!sceneArrowsContainerRef.current || sceneArrowsContainerRef.current.children.length === 0) {
            return;
        }

        const rect = canvas.getBoundingClientRect();
        mouseRef.current.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
        mouseRef.current.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

        raycasterRef.current.setFromCamera(mouseRef.current, currentCamera);
        
        const meshesToIntersect: THREE.Mesh[] = [];
        sceneArrowsContainerRef.current.traverse((child) => {
            if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).userData.type === 'navigation_arrow_mesh') {
                meshesToIntersect.push(child as THREE.Mesh);
            }
        });

        const intersects = raycasterRef.current.intersectObjects(meshesToIntersect);

        if (intersects.length > 0) {
            const firstIntersectedMesh = intersects[0].object as THREE.Mesh;
            const parentPivot = firstIntersectedMesh.parent as THREE.Object3D;

            if (parentPivot && parentPivot.userData.type === 'navigation_arrow_pivot' && parentPivot.userData.panoId) {
                const newPanoId = parentPivot.userData.panoId;
                try {
                    // Step 1: Animate camera using the arrow mesh's direction
                    if (cameraRef.current && controlsRef.current && streetViewMetadata) {
                        await animateCameraToDirection(cameraRef.current, controlsRef.current, firstIntersectedMesh, streetViewMetadata);
                    }

                    // Step 2: Apply prefetched tiles (instant)
                    const hasPrefetchedTiles = applyPrefetchedTiles(newPanoId);
                    
                    // If no prefetched tiles, fetch them now (fallback)
                    if (!hasPrefetchedTiles) {
                        await preFetchLowResTiles(newPanoId);
                        applyPrefetchedTiles(newPanoId);
                    }

                    // Step 3: Run heavy navigation logic in background
                    if (game) {
                        runHeavyNavigationLogic(
                            newPanoId,
                            game,
                            supabase,
                            setGame,
                            updateGameCurrentPoint,
                            fetchStreetViewMetadata,
                            setShowStopsMap,
                            setEndGameScore,
                            setEndGameDuration,
                            setShowEndGameModal,
                            cameraRef,
                            lastYawRef
                        );
                    }
                } catch (err) {
                    console.error("[Navigation] Error during navigation:", err);
                }
            }
        }
    };

    canvas.addEventListener('click', handleClick);
    return () => {
        canvas.removeEventListener('click', handleClick);
    };
  }, [sessionToken, fetchPanoramaTiles, setPanoId, setStreetViewMetadata, streetViewMetadata]);
  // Note: cameraRef.current is used, but it's a ref. Adding it to deps can cause loops if not careful.
  // Relying on main useEffect to update cameraRef and this effect to re-run when its explicit deps change.
  // This effect should re-bind if canvas is re-created, which happens if main effect re-runs.
  // Adding `game` as a proxy for canvas/camera readiness for listener attachment.
  // So, deps could be [sessionToken, fetchPanoramaTiles, setPanoId, setStreetViewMetadata, game]

  // Final cleanup for shared arrow geometry on component unmount
  useEffect(() => {
    return () => {
        if (arrowShapeGeometryRef.current) {
            arrowShapeGeometryRef.current.dispose();
            arrowShapeGeometryRef.current = null;
        }
        // Clean up sceneArrowsContainerRef if it was created by this specific instance of the main effect
        // The main effect's cleanup handles removing from scene.
        // sceneArrowsContainerRef.current = null;
    }
  }, []);

  // Function to update the game's current_point in Supabase
  const updateGameCurrentPoint = async (lat: number, lng: number) => {
    if (!game) return;
    const { error } = await supabase
      .from("games")
      .update({ current_point: { lat, lng } })
      .eq("id", game.id);
    if (error) {
    }
  };

  // Update minimap position when game or currentYaw changes
  useEffect(() => {
    if (game?.current_point) {
      updateMinimapPosition(game.current_point.lat, game.current_point.lng, currentYaw);
    }
  }, [game?.current_point, currentYaw, updateMinimapPosition]);

  // Show minimap when game is loaded and hide it when component unmounts or modal is open
  useEffect(() => {
    if (game) {
      setShowMinimap(!showStopsMap && !showEndGameModal);
    }
    return () => {
      setShowMinimap(false);
    };
  }, [game, setShowMinimap, showStopsMap, showEndGameModal]);

  // Compass directions and helpers
  const compassLabels = [
    { label: 'N', angle: 0 },
    { label: 'NE', angle: 45 },
    { label: 'E', angle: 90 },
    { label: 'SE', angle: 135 },
    { label: 'S', angle: 180 },
    { label: 'SW', angle: 225 },
    { label: 'W', angle: 270 },
    { label: 'NW', angle: 315 },
  ];
  // Generate ticks and labels for the tape
  const ticks = Array.from({ length: 360 / 5 }, (_, i) => i * 5);
  // For seamless wrap, repeat the pattern 3 times
  const repeatedTicks = [...ticks, ...ticks.map(t => t + 360), ...ticks.map(t => t + 720)];
  // Find the closest label to the current yaw
  const yawDeg = (THREE.MathUtils.radToDeg(currentYaw) + 360) % 360;
  // Center the tape so the current yaw is in the middle
  const tapeWidth = 340; // px (smaller)
  const tickSpacing = tapeWidth / 36; // 36 ticks (180deg span)
  // Offset so that the center tick (at yawDeg+360) is in the middle
  const tapeOffset = -(((yawDeg + 360) / 5) * tickSpacing - tapeWidth / 2);

  // Add helper function for calculating final distance
  const calculateFinalDistance = (currentPoint: { lat: number; lng: number }, endPoint: { lat: number; lng: number }) => {
    return haversineDistance(currentPoint, endPoint);
  };

  // Add helper function to update game status
  const updateGameStatus = async (gameId: string, status: string) => {
    try {
      const { error } = await supabase
        .from("games")
        .update({ status })
        .eq("id", gameId);
      
      if (error) {
        console.error("Error updating game status:", error);
        toast({
          title: "Error",
          description: "Failed to update game status. Please try again.",
          variant: "destructive",
        });
      } else {
        // Update local game state
        setGame(prev => prev ? { ...prev, status } : prev);
      }
    } catch (err) {
      console.error("Error updating game status:", err);
      toast({
        title: "Error",
        description: "An unexpected error occurred while updating game status.",
        variant: "destructive",
      });
    }
  };

  if (loading && !initialPanoramaReady) {
    return (
      <div className="flex min-h-screen items-center justify-center">
        <div className="text-center">
          <Compass className="mx-auto size-12 animate-spin text-primary" />
          <p className="mt-4 text-lg">Loading game...</p>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex min-h-screen items-center justify-center">
        <div className="text-center">
          <p className="text-lg text-destructive">Error: {error}</p>
          <Button
            variant="outline"
            className="mt-4"
            onClick={() => window.location.href = "/"}
          >
            Return Home
          </Button>
        </div>
      </div>
    );
  }

  if (!game) {
    return (
      <div className="flex min-h-screen items-center justify-center">
        <div className="text-center">
          <p className="text-lg">Game not found</p>
          <Button
            variant="outline"
            className="mt-4"
            onClick={() => window.location.href = "/"}
          >
            Return Home
          </Button>
        </div>
      </div>
    );
  }

  return (
    <div className="fixed inset-0 w-full h-full">
      {/* Compass Tape Bar */}
      <div className="fixed top-3 left-1/2 z-40" style={{ transform: 'translateX(-50%)' }}>
        <div className="relative flex items-center justify-center" style={{ width: tapeWidth + 20 }}>
          {/* Pill background */}
          <div className="absolute left-0 right-0 h-8 bg-gradient-to-r from-gray-600/80 to-gray-700/80 rounded-full shadow-lg" style={{ zIndex: 1 }} />
          {/* Tape */}
          <div
            className="relative flex items-center h-8 overflow-hidden"
            style={{ width: tapeWidth, zIndex: 2 }}
          >
            <div
              className="absolute flex items-center h-8"
              style={{ left: tapeOffset, transition: 'left 0.15s cubic-bezier(.4,2,.6,1)', minWidth: 2700 }}
            >
              {repeatedTicks.map((deg, i) => {
                const labelObj = compassLabels.find(l => l.angle === (deg % 360));
                const isMajor = deg % 45 === 0;
                return (
                  <div key={deg + '-' + i} className="flex flex-col items-center" style={{ width: tickSpacing }}>
                    <div
                      className={
                        isMajor
                          ? 'w-0.5 h-4 bg-white mb-0.5'
                          : 'w-0.5 h-2 bg-white/60 mb-0.5'
                      }
                    />
                    {labelObj ? (
                      <span
                        className={
                          Math.abs((((deg % 360) - yawDeg + 360) % 360)) < 3
                            ? 'font-extrabold text-xs text-white drop-shadow'
                            : 'font-bold text-xs text-white/80'
                        }
                        style={{ letterSpacing: 1 }}
                      >
                        {labelObj.label}
                      </span>
                    ) : (
                      <span className="h-3" />
                    )}
                  </div>
                );
              })}
            </div>
          </div>
          {/* Center marker (triangle) */}
          <div className="absolute left-1/2 top-0" style={{ transform: 'translateX(-50%)', zIndex: 3 }}>
            <svg width="12" height="7" viewBox="0 0 12 7">
              <polygon points="6,0 12,7 0,7" fill="#e3342f" />
            </svg>
          </div>
        </div>
      </div>

      {/* Add Give Up button */}
      <div className="fixed top-3 right-3 z-40">
        <Button
          variant="destructive"
          onClick={() => {
            if (game && game.current_point && game.end_point) {
              const distance = calculateFinalDistance(game.current_point, game.end_point);
              setFinalDistance(distance);
              setShowGiveUpModal(true);
            }
          }}
        >
          Give Up
        </Button>
      </div>

      <div 
        ref={panoramaContainerRef} 
        className="w-full h-full"
        style={{ 
          minHeight: '600px',
          position: 'relative',
          backgroundColor: '#000'
        }}
      />

      {/* Add Give Up Modal */}
      {showGiveUpModal && finalDistance !== null && (
        <div style={{
          position: 'fixed',
          top: '0',
          left: '0',
          right: '0',
          bottom: '0',
          backgroundColor: 'rgba(0, 0, 0, 0.5)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          zIndex: 50,
        }}>
          <div style={{
            backgroundColor: 'white',
            padding: '2rem',
            borderRadius: '0.5rem',
            boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
            textAlign: 'center',
            color: 'black',
            minWidth: '300px',
          }}>
            <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>Game Over</h2>
            <p style={{ marginBottom: '1.5rem' }}>
              Final distance to destination: {Math.round(finalDistance)} meters
            </p>
            <div style={{ display: 'flex', justifyContent: 'space-around' }}>
              <Button variant="outline" onClick={async () => {
                if (game) {
                  await updateGameStatus(game.id, 'cancelled');
                }
                setShowGiveUpModal(false);
                window.location.href = "/"; // Exit
              }}>Exit</Button>
              <Button onClick={async () => {
                if (game) {
                  await updateGameStatus(game.id, 'cancelled');
                }
                setShowGiveUpModal(false);
                window.location.href = "/"; // New Game
              }}>New Game</Button>
            </div>
          </div>
        </div>
      )}

      {showEndGameModal && endGameScore !== null && endGameDuration !== null && (
        <div style={{
          position: 'fixed',
          top: '0',
          left: '0',
          right: '0',
          bottom: '0',
          backgroundColor: 'rgba(0, 0, 0, 0.5)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          zIndex: 50,
        }}>
          <div style={{
            backgroundColor: 'white',
            padding: '2rem',
            borderRadius: '0.5rem',
            boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
            textAlign: 'center',
            color: 'black',
            minWidth: '300px',
            maxWidth: '800px',
            width: '90%',
          }}>
            <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>🎉 Congratulations! 🎉</h2>
            <p style={{ marginBottom: '0.5rem' }}>You've reached your destination!</p>
            <p style={{ marginBottom: '0.5rem' }}><strong>Score:</strong> {endGameScore}</p>
            <p style={{ marginBottom: '1.5rem' }}><strong>Time:</strong> {endGameDuration}</p>
            
            {/* Add Game Steps Map */}
            <div style={{ marginBottom: '1.5rem' }}>
              <h3 style={{ marginBottom: '1rem' }}>Your Journey</h3>
              <GameStepsMap gameId={game?.id || ''} />
            </div>

            <div style={{ display: 'flex', justifyContent: 'space-around' }}>
              <Button variant="outline" onClick={() => {
                setShowEndGameModal(false);
                window.location.href = "/"; // Exit
              }}>Exit</Button>
              <Button onClick={() => {
                setShowEndGameModal(false);
                window.location.href = "/"; // New Game
              }}>New Game</Button>
            </div>
          </div>
        </div>
      )}
      {/* Display Public Transport Routes or "No Nearby Stops" message */}
      {hasAttemptedStopFetch && (
        <div style={{
          position: 'fixed',
          bottom: '20px',
          left: '50px',
          backgroundColor: 'rgba(0, 0, 0, 0.4)',
          color: '#1a1a1a',
          padding: '10px 15px',
          borderRadius: '8px',
          zIndex: 100,
          maxWidth: 'calc(100vw - 40px)',
          maxHeight: '30vh',
          overflowY: 'auto',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
        }}>
          {displayedRoutes.length > 0 ? (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '5px 10px' }}>
              {displayedRoutes.map((route, index) => (
                <span 
                  key={index} 
                  style={{
                    backgroundColor: 'rgba(255, 255, 255, 0.9)',
                    padding: '5px 10px',
                    borderRadius: '4px',
                    fontSize: '1.1rem',
                    fontWeight: '600',
                    cursor: 'pointer',
                    display: 'flex',
                    alignItems: 'center',
                    gap: '5px',
                    color: '#1a1a1a',
                    transition: 'background-color 0.2s ease'
                  }}
                  onClick={() => getStopsByRoute(route)}
                >
                  {route.type !== undefined && (
                    <img 
                      src={`/${route.type}.png`} 
                      alt={`Route type ${route.type}`}
                      style={{ width: '20px', height: '20px', objectFit: 'contain' }}
                    />
                  )}
                  {route.route_short_name}
                </span>
              ))}
            </div>
          ) : (
            <p style={{ margin: 0, fontSize: '1rem', color: '#1a1a1a' }}>No nearby stops</p>
          )}
        </div>
      )}
      <StopsMapModal
        isOpen={showStopsMap}
        onClose={() => setShowStopsMap(false)}
        stops={routeStops}
        routeName={selectedRoute || ''}
        game={game}
        directions={routeDirections}
        onNavigate={async (lat, lng) => {
          if (game) {
            await handleNavigation(
              lat,
              lng,
              game,
              supabase,
              setGame,
              updateGameCurrentPoint,
              fetchStreetViewMetadata,
              setShowStopsMap,
              setEndGameScore,
              setEndGameDuration,
              setShowEndGameModal,
              cameraRef,
              lastYawRef
            );
          }
        }}
        endLat={game?.end_point?.lat}
        endLng={game?.end_point?.lng}
      />
    </div>
  );
} 