<script>
  import { onMount, onDestroy } from 'svelte';
  import * as d3 from 'd3';
  import { feature } from 'topojson-client';
  import worldData from 'world-atlas/countries-50m.json';
  import { get } from 'svelte/store';
  import { journals } from '../stores/journalStore.js';
  import airplaneImg from '../../assets/airplane.png';
  import trainImg    from '../../assets/train.png';
  import busImg      from '../../assets/bus.png';
  import carImg      from '../../assets/car.png';
  import shipImg     from '../../assets/ship.png';
  import walkImg     from '../../assets/walk.png';

  let { onclose, onprogress, mode = 'map', onmodechange } = $props();

  const HOME_CODE = '203';

  const TRANSPORT_IMG = {
    flight: airplaneImg,
    train:  trainImg,
    bus:    busImg,
    car:    carImg,
    ship:   shipImg,
    walk:   walkImg,
  };
  const PLANE_SIZE = 28;

  const HOME_COLOR = '#8b5cf6';
  const VISITED_COLOR = '#22c55e';
  const ARC_COLOR = '#666666';
  const UNVISITED = '#ffffff';

  const TERRITORY_PARENT = {
    '016': '840', '060': '826', '086': '826', '092': '826', '136': '826',
    '184': '554', '234': '208', '238': '826', '239': '826', '248': '246',
    '258': '250', '260': '250', '304': '208', '316': '840', '334': '036',
    '446': '156', '500': '826', '531': '528', '533': '528', '534': '528',
    '540': '250', '570': '554', '574': '036', '580': '840', '612': '826',
    '630': '840', '652': '250', '654': '826', '660': '826', '663': '250',
    '666': '250', '796': '826', '831': '826', '832': '826', '833': '826',
    '850': '840', '876': '250',
  };

  function effId(d) { return TERRITORY_PARENT[d.id] || d.id; }

  let frameEl;
  let svg, gBase, gCountries, gAnim, pathFn, projection;
  let countryPaths;
  let homeFeature;
  let featuresById = {};
  let countriesData = [];
  let isCancelled = false;
  let isPlaying = $state(false);
  let isFinished = $state(false);
  let visitedCodes = new Set();
  let animId = 0;
  let currentDateLabel = $state('');

  function formatDateLabel(dateStr) {
    const d = new Date(dateStr);
    const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
    return `${months[d.getMonth()]} ${d.getFullYear()}`;
  }

  function computeArc(p1, p2) {
    const interp = d3.geoInterpolate(p1, p2);
    const raw = [];
    for (let i = 0; i <= 80; i++) {
      const t = i / 80;
      const pt = projection(interp(t));
      if (!pt) continue;
      raw.push({ t, x: pt[0], y: pt[1] });
    }
    if (raw.length < 2) return [];
    const first = raw[0], last = raw[raw.length - 1];
    const dist = Math.sqrt((last.x-first.x)**2 + (last.y-first.y)**2);
    const arcH = Math.max(40, Math.min(200, dist * 0.22));
    return raw.map(p => [p.x, p.y - arcH * Math.sin(Math.PI * p.t)]);
  }

  function planeTransform(x, y, angle, flip) {
    return `translate(${x},${y}) rotate(${angle})${flip ? ' scale(1,-1)' : ''}`;
  }

  function delay(ms) {
    return new Promise(resolve => { if (isCancelled) { resolve(); return; } setTimeout(resolve, ms); });
  }

  function setupProjection(width, height) {
    if (mode === 'map') {
      projection = d3.geoMercator();
      projection.fitSize([width, height], { type: 'Sphere' });
      const s = projection.scale() * 1.5;
      projection.scale(s).translate([width / 2, height * 0.70]);
    } else {
      const size = Math.min(width, height) * 0.92;
      projection = d3.geoOrthographic()
        .rotate([0, 0])
        .fitSize([size, size], { type: 'Sphere' })
        .translate([width / 2, height / 2]);
    }
    pathFn = d3.geoPath().projection(projection);
  }

  function renderMap() {
    gBase.selectAll('*').remove();
    gCountries.selectAll('*').remove();
    gAnim.selectAll('*').remove();

    const fillFn = d => {
      const id = effId(d);
      if (id === HOME_CODE) return visitedCodes.has(id) ? VISITED_COLOR : HOME_COLOR;
      if (visitedCodes.has(id)) return VISITED_COLOR;
      return UNVISITED;
    };

    if (mode === 'globe') {
      gBase.append('path').attr('class', 'sphere').datum({ type: 'Sphere' })
        .attr('d', pathFn).attr('fill', '#a4c8e0').attr('stroke', '#8b9bb0').attr('stroke-width', 1.5);
    }

    countryPaths = gCountries.selectAll('path')
      .data(countriesData, d => effId(d)).join('path')
      .attr('d', pathFn).attr('fill', fillFn)
      .attr('stroke', mode === 'globe' ? '#4a6a8c' : '#d4d4d4')
      .attr('stroke-width', mode === 'globe' ? 0.3 : 0.5);

    if (mode === 'map') renderMicrostates();
  }

  function renderMicrostates() {
    gBase.selectAll('.micro-state-j').remove();
    const fillFn = d => {
      const id = effId(d);
      if (id === HOME_CODE) return visitedCodes.has(id) ? VISITED_COLOR : HOME_COLOR;
      if (visitedCodes.has(id)) return VISITED_COLOR;
      return UNVISITED;
    };
    countryPaths.each(function(d) {
      if (effId(d) !== d.id) return;
      const { width, height } = this.getBBox();
      if (width < 4 && height < 4) {
        const [cx, cy] = pathFn.centroid(d);
        gBase.append('circle').attr('class', 'micro-state-j').datum(d)
          .attr('cx', cx).attr('cy', cy).attr('r', 2).attr('fill', fillFn(d))
          .attr('stroke', '#94a3b8').attr('stroke-width', 0.5);
      }
    });
  }

  function redrawBase() {
    countryPaths.attr('d', pathFn);
    if (mode === 'globe') gBase.select('.sphere').attr('d', pathFn);
  }

  function rotateGlobeTo(lon, lat, duration = 1500) {
    return new Promise(resolve => {
      if (isCancelled) { resolve(); return; }
      const current = projection.rotate();
      const interp = d3.geoInterpolate([-current[0], -current[1]], [lon, lat]);
      const timer = d3.timer(elapsed => {
        if (isCancelled) { timer.stop(); resolve(); return true; }
        const t = Math.min(elapsed / duration, 1);
        const point = interp(t);
        projection.rotate([-point[0], -point[1]]);
        redrawBase();
        if (t >= 1) { timer.stop(); resolve(); return true; }
      });
    });
  }

  function createArcEl(iconSrc) {
    const el = gAnim.append('path')
      .attr('fill', 'none').attr('stroke', ARC_COLOR)
      .attr('stroke-width', 2.5).attr('stroke-opacity', 0.8)
      .attr('stroke-linecap', 'round').attr('stroke-dasharray', '10, 6');
    const tip = gAnim.append('image')
      .attr('href', iconSrc).attr('width', PLANE_SIZE).attr('height', PLANE_SIZE)
      .attr('x', -PLANE_SIZE / 2).attr('y', -PLANE_SIZE / 2)
      .attr('preserveAspectRatio', 'xMidYMid meet').attr('opacity', 0);
    return { el, tip };
  }

  function animateIncrementalPath(el, tip, pts, duration, flip = false) {
    return new Promise(resolve => {
      const lineGen = d3.line().curve(d3.curveBasis);
      d3.timer(elapsed => {
        if (isCancelled) { resolve(); return true; }
        const t = Math.min(elapsed / duration, 1);
        const count = Math.max(2, Math.floor(t * (pts.length - 1)) + 1);
        const visible = pts.slice(0, count);
        if (visible.length >= 2) el.attr('d', lineGen(visible));
        if (visible.length > 0) {
          const last = visible[visible.length - 1];
          let angle = 0;
          if (visible.length >= 2) {
            const prev = visible[visible.length - 2];
            angle = Math.atan2(last[1] - prev[1], last[0] - prev[0]) * 180 / Math.PI;
          }
          tip.attr('transform', planeTransform(last[0], last[1], angle, flip)).attr('opacity', 1);
        }
        if (t >= 1) { resolve(); return true; }
      });
    });
  }

  function animateReprojectingArc(el, tip, geoPts, lineGen, duration, flip = false) {
    return new Promise(resolve => {
      const timer = d3.timer(elapsed => {
        if (isCancelled) { timer.stop(); resolve(); return true; }
        const t = Math.min(elapsed / duration, 1);
        const count = Math.max(2, Math.floor(t * (geoPts.length - 1)) + 1);
        const screenPts = geoPts.slice(0, count).map(p => projection(p)).filter(Boolean);
        if (screenPts.length >= 2) el.attr('d', lineGen(screenPts));
        if (screenPts.length > 0) {
          const last = screenPts[screenPts.length - 1];
          let angle = 0;
          if (screenPts.length >= 2) {
            const prev = screenPts[screenPts.length - 2];
            angle = Math.atan2(last[1] - prev[1], last[0] - prev[0]) * 180 / Math.PI;
          }
          tip.attr('transform', planeTransform(last[0], last[1], angle, flip)).attr('opacity', 1);
        }
        if (t >= 1) { timer.stop(); resolve(); return true; }
      });
    });
  }

  async function animateTrip(destCode, destFeature, transport = 'flight') {
    if (!homeFeature || !destFeature) return;
    const iconSrc = TRANSPORT_IMG[transport] ?? airplaneImg;
    const homeCentroid = d3.geoCentroid(homeFeature);
    const destCentroid = d3.geoCentroid(destFeature);
    if (mode === 'map') {
      await animateMapTrip(homeCentroid, destCentroid, destCode, iconSrc);
    } else {
      await animateGlobeTrip(homeCentroid, destCentroid, destCode, iconSrc);
    }
  }

  async function animateMapTrip(homeCentroid, destCentroid, destCode, iconSrc) {
    const pts = computeArc(homeCentroid, destCentroid);
    if (pts.length < 2) return;
    const { el: outEl, tip: outTip } = createArcEl(iconSrc);
    await animateIncrementalPath(outEl, outTip, pts, 2500, pts[pts.length-1][0] < pts[0][0]);
    if (isCancelled) return;
    outEl.remove(); outTip.remove();
    countryPaths.filter(d => effId(d) === destCode).transition().duration(500).attr('fill', VISITED_COLOR);
    visitedCodes.add(destCode);
    gBase.selectAll('.micro-state-j').filter(d => effId(d) === destCode).transition().duration(500).attr('fill', VISITED_COLOR);
    await delay(800);
    if (isCancelled) return;
    const revPts = [...pts].reverse();
    const { el: retEl, tip: retTip } = createArcEl(iconSrc);
    await animateIncrementalPath(retEl, retTip, revPts, 2200, revPts[revPts.length-1][0] < revPts[0][0]);
    if (isCancelled) return;
    retEl.remove(); retTip.remove();
    await delay(300);
  }

  async function animateGlobeTrip(homeCentroid, destCentroid, destCode, iconSrc) {
    const interp = d3.geoInterpolate(homeCentroid, destCentroid);
    const geoPts = Array.from({ length: 81 }, (_, i) => interp(i / 80));
    const dur = Math.round(1500 + d3.geoDistance(homeCentroid, destCentroid) * 2500);
    const lineGen = d3.line().curve(d3.curveBasis);
    const { el: outEl, tip: outTip } = createArcEl(iconSrc);
    const outGcs = geoPts.map(p => projection(p)).filter(Boolean);
    await Promise.all([
      rotateGlobeTo(destCentroid[0], destCentroid[1], dur),
      animateReprojectingArc(outEl, outTip, geoPts, lineGen, dur, outGcs.length >= 2 && outGcs[outGcs.length-1][0] < outGcs[0][0]),
    ]);
    if (isCancelled) return;
    outEl.remove(); outTip.remove();
    countryPaths.filter(d => effId(d) === destCode).transition().duration(500).attr('fill', VISITED_COLOR);
    visitedCodes.add(destCode);
    await delay(600);
    if (isCancelled) return;
    const revGeoPts = [...geoPts].reverse();
    const { el: retEl, tip: retTip } = createArcEl(iconSrc);
    const retGcs = revGeoPts.map(p => projection(p)).filter(Boolean);
    await Promise.all([
      rotateGlobeTo(homeCentroid[0], homeCentroid[1], dur),
      animateReprojectingArc(retEl, retTip, revGeoPts, lineGen, dur, retGcs.length >= 2 && retGcs[retGcs.length-1][0] < retGcs[0][0]),
    ]);
    if (isCancelled) return;
    retEl.remove(); retTip.remove();
    await delay(300);
  }

  async function startJourney() {
    const myId = ++animId;
    isPlaying = true; isFinished = false; isCancelled = false; visitedCodes = new Set();

    const width = frameEl.clientWidth, height = frameEl.clientHeight;
    svg.selectAll('*').remove();
    gBase = svg.append('g'); gCountries = svg.append('g'); gAnim = svg.append('g');
    setupProjection(width, height);
    if (mode === 'globe' && homeFeature) {
      const c = d3.geoCentroid(homeFeature);
      projection.rotate([-c[0], -c[1]]);
      pathFn = d3.geoPath().projection(projection);
    }
    renderMap();

    const nameToId = Object.fromEntries(Object.entries(featuresById).filter(([,f]) => f.properties?.name).map(([id, f]) => [f.properties.name, id]));
    const entries = get(journals).slice().sort((a, b) => a.date.localeCompare(b.date));
    const trips = entries.length > 0
      ? entries.map(e => ({
          countryName: e.location.country,
          countryCode: nameToId[e.location.country] ?? null,
          city: e.location.cities?.[0] ?? e.location.country,
          transport: e.transport ?? 'flight',
          date: e.date,
        })).filter(t => t.countryCode)
      : [
          { countryName: 'Japan', countryCode: '392', city: 'Tokyo', transport: 'flight', date: '2024-03-15' },
          { countryName: 'France', countryCode: '250', city: 'Paris', transport: 'flight', date: '2024-06-20' },
          { countryName: 'Spain', countryCode: '724', city: 'Barcelona', transport: 'flight', date: '2024-09-10' },
          { countryName: 'United States of America', countryCode: '840', city: 'New York', transport: 'flight', date: '2025-01-05' },
          { countryName: 'Thailand', countryCode: '764', city: 'Bangkok', transport: 'flight', date: '2025-04-18' },
          { countryName: 'Australia', countryCode: '036', city: 'Sydney', transport: 'flight', date: '2025-08-22' },
        ];

    for (let i = 0; i < trips.length; i++) {
      if (isCancelled || myId !== animId) break;
      const trip = trips[i];
      if (trip.date) currentDateLabel = formatDateLabel(trip.date);
      const destFeature = featuresById[trip.countryCode];
      if (!destFeature) continue;
      if (onprogress) onprogress({ index: i + 1, total: trips.length, label: `${trip.city}, ${trip.countryName}` });
      await animateTrip(trip.countryCode, destFeature, trip.transport);
    }

    if (!isCancelled && myId === animId) {
      isFinished = true; isPlaying = false;
      if (onprogress) onprogress({ index: trips.length, total: trips.length, label: 'Journey complete!' });
    } else if (myId === animId) { isPlaying = false; }
  }

  function stopJourney() { isCancelled = true; isPlaying = false; }
  function replay() { stopJourney(); setTimeout(() => startJourney(), 100); }
  function switchMode(target) {
    if (target === mode) return;
    onmodechange?.(target);
    stopJourney();
    setTimeout(() => startJourney(), 100);
  }
  function close() { stopJourney(); onclose?.(); }

  onMount(() => {
    const width = frameEl.clientWidth, height = frameEl.clientHeight;
    setupProjection(width, height);

    countriesData = feature(worldData, worldData.objects.countries)
      .features.filter(f => (f.id || f.properties.name === 'Kosovo') && f.id !== '010');
    countriesData.forEach(f => { if (!f.id) f.id = 'XK'; });
    for (const f of countriesData) featuresById[effId(f)] = f;
    homeFeature = featuresById[HOME_CODE];

    svg = d3.select(frameEl).append('svg').attr('width', width).attr('height', height).style('cursor', 'default');
    gBase = svg.append('g'); gCountries = svg.append('g'); gAnim = svg.append('g');
    renderMap();

    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        svg.attr('width', width).attr('height', height);
        const prevRotate = mode === 'globe' ? projection.rotate() : null;
        if (mode === 'map') {
          projection = d3.geoMercator();
          projection.fitSize([width, height], { type: 'Sphere' });
          projection.scale(projection.scale() * 1.5).translate([width / 2, height * 0.70]);
        } else {
          const size = Math.min(width, height) * 0.92;
          projection = d3.geoOrthographic().rotate(prevRotate).fitSize([size, size], { type: 'Sphere' }).translate([width / 2, height / 2]);
        }
        pathFn = d3.geoPath().projection(projection);
        redrawBase();
        if (mode === 'map') renderMicrostates();
      }
    });
    observer.observe(frameEl);
    startJourney();
    return () => { stopJourney(); observer.disconnect(); if (svg) svg.remove(); };
  });
</script>

<div bind:this={frameEl} class="journey-frame" class:globe-mode={mode === 'globe'}>
  <div class="top-label">
    {#if isFinished}Journey complete!{:else if currentDateLabel}{currentDateLabel}{/if}
  </div>
  <div class="control-bar">
    <button class="control-btn" onclick={replay}>⟳ Replay</button>
    <button class="control-btn" onclick={() => switchMode(mode === 'map' ? 'globe' : 'map')}>
      {mode === 'map' ? '🌍 Globe view' : '🗺 Map view'}
    </button>
    <button class="control-btn" onclick={close}>✕ Close</button>
  </div>
</div>

<style>
  .journey-frame { width: 100%; height: 100%; overflow: hidden; position: relative; background: #a4c8e0; }
  .journey-frame.globe-mode { background: #ffffff; }
  .journey-frame :global(svg) { display: block; }

  .top-label {
    position: absolute; top: 16px; left: 16px; z-index: 10;
    background: rgba(0,0,0,0.65); color: #fff;
    font-family: var(--heading, sans-serif); font-size: 16px; font-weight: 600;
    padding: 10px 24px; border-radius: 24px; white-space: nowrap;
    letter-spacing: 0.04em; min-width: 200px; text-align: center;
  }

  .control-bar {
    position: absolute; bottom: 24px; right: 24px; z-index: 10;
    display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
  }

  .control-btn {
    padding: 10px 24px; border: none; border-radius: 24px;
    background: #8b5cf6; color: #fff; font-size: 14px; font-weight: 600;
    cursor: pointer; transition: background 0.15s ease; white-space: nowrap; font-family: inherit;
  }
  .control-btn:hover { background: #7c3aed; }
  .control-btn:active { transform: scale(0.96); }
</style>
