"use client";

import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import "maplibre-gl/dist/maplibre-gl.css";

import {
  INIT_VIEW,
  BASEMAPS,
  TRAIL_LENGTH_HOURS,
  TRAIL_STEP_STRIDE,
} from "@/lib/constants";
import { fitBounds } from "@math.gl/web-mercator";
import {
  buildScatterData,
  buildTrails,
  buildTripsWithTime,
} from "@/lib/trajectoryData";
import type { Meta, PointDatum } from "@/types/trajectory";
import { useMetadata } from "@/hooks/useMetadata";
import { useBlocks } from "@/hooks/useBlocks";
import { useSimulation } from "@/hooks/useSimulation";
import { useMapLayers } from "@/hooks/useMapLayers";
import { IntroDialog, shouldShowIntro } from "@/components/IntroDialog";
import { LoadingCard } from "@/components/LoadingCard";
import { MapHeader } from "@/components/MapHeader";
import { TimePlayer } from "@/components/TimePlayer";
import type { ViewState } from "@/components/MapView";

const MapView = dynamic(
  () => import("@/components/MapView").then((m) => ({ default: m.MapView })),
  { ssr: false, loading: () => <div className="flex h-full w-full items-center justify-center bg-background text-muted-foreground">Loading map…</div> }
);

const BLOCK_SIZE = 24;

export default function TrajectoryMap() {
  const [mounted, setMounted] = useState(false);
  const [canvasReady, setCanvasReady] = useState(false);
  const [showIntro, setShowIntro] = useState(true);
  const [viewState, setViewState] = useState<ViewState>(INIT_VIEW);
  const [basemapId, setBasemapId] = useState<
    (typeof BASEMAPS)[number]["id"]
  >("dark");
  const [showTrails, setShowTrails] = useState(true);
  const [showActiveParticles, setShowActiveParticles] = useState(true);
  const [showBeachedParticles, setShowBeachedParticles] = useState(true);
  const [filtersOpen, setFiltersOpen] = useState(false);
  const [infoOpen, setInfoOpen] = useState(false);
  const [selectedParticleId, setSelectedParticleId] = useState<number | null>(
    null
  );
  const [accumulatedBeached, setAccumulatedBeached] = useState<
    Map<number, [number, number]>
  >(new Map());
  const prevSimIdRef = useRef<string | null>(null);
  const mapContainerRef = useRef<HTMLDivElement>(null);

  const searchParams = useSearchParams();
  const simId = searchParams.get("sim") ?? "default";
  const meta = useMetadata(simId);
  const { blocks, loadProgress, ready } = useBlocks(meta, simId);
  const {
    step,
    setStep,
    playing,
    speedIdx,
    togglePlay,
    stepBack,
    stepForward,
    restart,
    cycleSpeed,
    onSlider,
    maxStep,
    day,
    hour,
    dateLabel,
    startLabel,
    endLabel,
  } = useSimulation(meta);

  useEffect(() => setMounted(true), []);

  useEffect(() => {
    setShowIntro((prev) => (prev ? shouldShowIntro() : false));
  }, []);

  // Defer DeckGL mount until after first paint so WebGL context is ready (avoids device.limits undefined in some browsers).
  useEffect(() => {
    if (!mounted) return;
    let timeoutId: ReturnType<typeof setTimeout> | undefined;
    let rafId2: number | null = null;
    const rafId = requestAnimationFrame(() => {
      rafId2 = requestAnimationFrame(() => {
        timeoutId = setTimeout(() => setCanvasReady(true), 150);
      });
    });
    return () => {
      cancelAnimationFrame(rafId);
      if (rafId2 != null) cancelAnimationFrame(rafId2);
      if (timeoutId != null) clearTimeout(timeoutId);
    };
  }, [mounted]);

  const blockIdx = Math.floor(step / BLOCK_SIZE);
  const currentBlock = blocks[blockIdx];
  const stepStart = currentBlock?.step_start ?? blockIdx * BLOCK_SIZE;
  const frameIdx = step - stepStart;
  const currentFrame =
    currentBlock?.frames &&
    frameIdx >= 0 &&
    frameIdx < currentBlock.frames.length
      ? currentBlock.frames[frameIdx]
      : null;

  // Reset accumulated beached when changing simulation
  useEffect(() => {
    if (prevSimIdRef.current !== null && prevSimIdRef.current !== simId) {
      setAccumulatedBeached(new Map());
    }
    prevSimIdRef.current = simId;
  }, [simId]);

  // Accumulate beached particles from each frame so they don't disappear in later steps
  useEffect(() => {
    if (!currentFrame) return;
    setAccumulatedBeached((prev) => {
      const next = new Map(prev);
      for (let i = 0; i < currentFrame.length; i++) {
        if (currentFrame[i][4] === 1) {
          next.set(i, [currentFrame[i][0], currentFrame[i][1]]);
        }
      }
      return next;
    });
  }, [currentFrame]);

  const scatterData = useMemo(
    () => (currentFrame ? buildScatterData(currentFrame) : []),
    [currentFrame]
  );
  const trailData = useMemo(
    () =>
      ready && meta
        ? buildTrails(
            blocks,
            step,
            meta.num_particles,
            meta.release_steps,
            TRAIL_STEP_STRIDE
          )
        : [],
    [blocks, step, ready, meta]
  );
  const tripDataWithTime = useMemo(
    () =>
      ready && meta
        ? buildTripsWithTime(
            blocks,
            step,
            meta.num_particles,
            meta.release_steps,
            TRAIL_STEP_STRIDE
          )
        : [],
    [blocks, step, ready, meta]
  );

  const currentTimeSec = step * 3600;
  const trailLengthSec =
    Math.min(meta?.num_steps ?? 72, TRAIL_LENGTH_HOURS) * 3600;

  const accumulatedBeachedPts = useMemo(
    () =>
      Array.from(accumulatedBeached.entries()).map(([id, position]) => ({
        id,
        position,
        beached: true as const,
        speed: 0,
      })),
    [accumulatedBeached]
  );
  const beachedN = accumulatedBeached.size;
  const activeN = (meta?.num_particles ?? 0) - beachedN;

  /** Bounds [[minLon, minLat], [maxLon, maxLat]] for all current particles (frame + beached). */
  const particleBounds = useMemo((): [[number, number], [number, number]] | null => {
    const positions: [number, number][] = [
      ...scatterData.map((d) => d.position),
      ...accumulatedBeachedPts.map((d) => d.position),
    ].filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
    if (positions.length === 0) return null;
    let minLon = Infinity, maxLon = -Infinity, minLat = Infinity, maxLat = -Infinity;
    for (const [lon, lat] of positions) {
      minLon = Math.min(minLon, lon); maxLon = Math.max(maxLon, lon);
      minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat);
    }
    if (!Number.isFinite(minLon) || !Number.isFinite(maxLon) || !Number.isFinite(minLat) || !Number.isFinite(maxLat)) return null;
    const pad = 0.02;
    if (maxLon - minLon < pad) { minLon -= pad / 2; maxLon += pad / 2; }
    if (maxLat - minLat < pad) { minLat -= pad / 2; maxLat += pad / 2; }
    return [[minLon, minLat], [maxLon, maxLat]];
  }, [scatterData, accumulatedBeachedPts]);

  const fitToParticles = useCallback(() => {
    if (!particleBounds || !mapContainerRef.current) return;
    const el = mapContainerRef.current;
    const { width, height } = el.getBoundingClientRect();
    const padding = 40;
    if (width <= padding * 2 || height <= padding * 2) return;
    try {
      const view = fitBounds({
        width,
        height,
        bounds: particleBounds,
        padding,
        minExtent: 0.01,
        maxZoom: 14,
      });
      if (
        Number.isFinite(view.zoom) &&
        Number.isFinite(view.longitude) &&
        Number.isFinite(view.latitude)
      ) {
        setViewState((prev) => ({ ...prev, ...view }));
      }
    } catch {
      const [[minLon, minLat], [maxLon, maxLat]] = particleBounds;
      const lon = (minLon + maxLon) / 2;
      const lat = (minLat + maxLat) / 2;
      if (Number.isFinite(lon) && Number.isFinite(lat)) {
        setViewState((prev) => ({
          ...prev,
          longitude: lon,
          latitude: lat,
          zoom: Math.min(prev.zoom ?? 10, 12),
        }));
      }
    }
  }, [particleBounds]);

  const layers = useMapLayers(
    currentFrame,
    scatterData,
    trailData,
    tripDataWithTime,
    currentTimeSec,
    trailLengthSec,
    meta,
    step,
    selectedParticleId,
    showTrails,
    showActiveParticles,
    showBeachedParticles,
    accumulatedBeachedPts
  );

  const currentBasemap = useMemo(
    () => BASEMAPS.find((b) => b.id === basemapId) ?? BASEMAPS[0],
    [basemapId]
  );

  const safeViewState = useMemo(() => {
    const { longitude, latitude, zoom } = viewState;
    if (
      Number.isFinite(longitude) &&
      Number.isFinite(latitude) &&
      Number.isFinite(zoom)
    ) {
      return viewState;
    }
    return {
      ...viewState,
      longitude: Number.isFinite(longitude) ? longitude : INIT_VIEW.longitude,
      latitude: Number.isFinite(latitude) ? latitude : INIT_VIEW.latitude,
      zoom: Number.isFinite(zoom) ? zoom : INIT_VIEW.zoom,
    };
  }, [viewState]);

  const onMapClick = useCallback(({ object }: { object?: PointDatum }) => {
    if (object && typeof object.id === "number") {
      setSelectedParticleId((prev) =>
        prev === object.id ? null : object.id
      );
    } else {
      setSelectedParticleId(null);
    }
  }, []);

  const getTooltip = useCallback(
    ({ object }: { object?: PointDatum }) => {
      if (!object || !meta) return null;
      const origin = meta.seed_names[meta.origins[object.id]] ?? "?";
      return {
        text: object.beached
          ? `#${object.id} · Beached · Origin: ${origin} (click to select)`
          : `#${object.id} · ${object.speed.toFixed(3)} m/s · Origin: ${origin} (click to select)`,
      };
    },
    [meta]
  );

  return (
    <>
      <IntroDialog open={showIntro} onOpenChange={setShowIntro} />

      {!ready && (
        <LoadingCard meta={meta} loadProgress={loadProgress} />
      )}

      {ready && (
        <div className="relative h-full w-full">
          {!mounted || !canvasReady ? (
            <div className="flex h-full w-full items-center justify-center bg-background text-muted-foreground">
              Preparing map…
            </div>
          ) : (
            <div ref={mapContainerRef} className="absolute inset-0">
              <MapView
                viewState={safeViewState}
                setViewState={setViewState}
                layers={layers}
                onClick={onMapClick}
                getTooltip={getTooltip}
                mapStyle={currentBasemap.url}
                onFitToParticles={particleBounds ? fitToParticles : undefined}
              />

              <MapHeader
                basemapId={basemapId}
                setBasemapId={setBasemapId}
                day={day}
                hour={hour}
                dateLabel={dateLabel}
                activeN={activeN}
                beachedN={beachedN}
                filtersOpen={filtersOpen}
                setFiltersOpen={setFiltersOpen}
                infoOpen={infoOpen}
                setInfoOpen={setInfoOpen}
                showTrails={showTrails}
                setShowTrails={setShowTrails}
                showActiveParticles={showActiveParticles}
                setShowActiveParticles={setShowActiveParticles}
                showBeachedParticles={showBeachedParticles}
                setShowBeachedParticles={setShowBeachedParticles}
              />

              <TimePlayer
                step={step}
                onSlider={onSlider}
                maxStep={maxStep}
                day={day}
                hour={hour}
                metaNumSteps={meta?.num_steps ?? 0}
                metaNumParticles={meta?.num_particles ?? 0}
                playing={playing}
                speedIdx={speedIdx}
                togglePlay={togglePlay}
                stepBack={stepBack}
                stepForward={stepForward}
                restart={restart}
                cycleSpeed={cycleSpeed}
                dateLabel={dateLabel}
                startLabel={startLabel}
                endLabel={endLabel}
              />
            </div>
          )}
        </div>
      )}
    </>
  );
}
