/*
 * DigestGlobe — a rotating "holo globe" for the political digest section.
 *
 * SVG orthographic projection, not CSS 3D transforms. CSS 3D can fake a sphere
 * (stacked rotated rings, a textured div under perspective), but there is then no
 * honest way to answer "where on screen is 34.05N, 118.24W" — you would be
 * eyeballing marker placement against a fake, and it drifts as soon as anything
 * about the container changes. Orthographic projection is four lines of
 * trigonometry, gives exact marker positions and exact back-of-globe visibility,
 * needs no 3D library, and the same maths projects both the markers and the
 * coastline outline so they can never disagree.
 *
 * No external dependencies, no CDN, no network access of any kind — the world
 * outline below is inlined as coarse lat/lon polylines. It is deliberately
 * low-poly: recognisable as Earth, not a cartographic reference.
 */

(function (global) {
  'use strict';

  var RADIUS = 95;              // in viewBox units; viewBox is -100..100 on both axes
  var DEG = Math.PI / 180;

  // [lon, lat] rings, coarse. Antarctica is omitted deliberately: at this vertex
  // count it reads as noise around the south limb rather than as a continent.
  var WORLD = [
    // Africa
    [[-17, 14], [-16, 20], [-5, 30], [10, 37], [20, 32], [32, 31], [43, 12], [51, 12],
     [41, -2], [40, -16], [35, -24], [20, -35], [18, -33], [12, -17], [9, 4], [-8, 4],
     [-13, 9], [-17, 14]],
    // Eurasia
    [[-10, 36], [-2, 43], [3, 43], [9, 44], [16, 41], [23, 38], [28, 41], [35, 36],
     [36, 31], [43, 30], [48, 30], [57, 25], [67, 24], [72, 19], [77, 8], [80, 13],
     [87, 21], [92, 22], [97, 17], [100, 13], [104, 10], [109, 15], [117, 23],
     [122, 31], [122, 40], [127, 39], [129, 43], [135, 48], [140, 53], [143, 59],
     [160, 61], [170, 66], [180, 68], [160, 70], [140, 73], [110, 74], [80, 73],
     [60, 71], [40, 68], [30, 70], [20, 70], [10, 64], [5, 58], [0, 51], [-5, 48],
     [-10, 43], [-9, 39], [-6, 37], [-10, 36]],
    // North America
    [[-168, 66], [-160, 58], [-150, 60], [-135, 58], [-125, 49], [-122, 37], [-117, 32],
     [-105, 23], [-97, 16], [-94, 18], [-90, 21], [-84, 22], [-81, 25], [-81, 31],
     [-76, 35], [-70, 42], [-66, 45], [-60, 47], [-56, 52], [-64, 60], [-78, 62],
     [-85, 70], [-95, 70], [-110, 69], [-125, 70], [-140, 70], [-155, 71], [-168, 66]],
    // South America
    [[-81, -5], [-79, 0], [-76, 8], [-71, 12], [-62, 10], [-52, 5], [-50, 0], [-44, -2],
     [-38, -5], [-35, -8], [-39, -16], [-48, -25], [-54, -34], [-58, -38], [-62, -40],
     [-65, -45], [-68, -52], [-75, -52], [-73, -45], [-73, -37], [-71, -30], [-70, -20],
     [-76, -14], [-81, -5]],
    // Australia
    [[113, -22], [114, -27], [118, -34], [126, -32], [133, -32], [138, -35], [145, -38],
     [150, -37], [153, -28], [146, -19], [142, -11], [136, -12], [130, -11], [125, -14],
     [118, -20], [113, -22]],
    // Greenland
    [[-45, 60], [-52, 65], [-55, 70], [-60, 76], [-50, 82], [-30, 83], [-22, 74],
     [-30, 68], [-42, 61], [-45, 60]]
  ];

  var ICONS = {
    'hammer-sickle': '☭',
    star: '★',
    default: '●'
  };

  var SVG_NS = 'http://www.w3.org/2000/svg';

  function svgEl(name, attrs) {
    var el = document.createElementNS(SVG_NS, name);
    Object.keys(attrs || {}).forEach(function (key) {
      el.setAttribute(key, attrs[key]);
    });
    return el;
  }

  // Orthographic projection about a rotating central meridian.
  // Returns {x, y} in viewBox units and z, the cosine of angular distance from
  // the projection centre — z <= 0 means the point is on the far side of the globe.
  function project(lat, lon, lambda0) {
    var phi = lat * DEG;
    var lam = (lon - lambda0) * DEG;
    return {
      x: RADIUS * Math.cos(phi) * Math.sin(lam),
      y: -RADIUS * Math.sin(phi),
      z: Math.cos(phi) * Math.cos(lam)
    };
  }

  function DigestGlobe(container, options) {
    options = options || {};

    this.container = container;
    this.lambda0 = typeof options.longitude === 'number' ? options.longitude : 0;
    this.speed = typeof options.speed === 'number' ? options.speed : 4; // degrees/second
    this.markers = [];

    container.classList.add('digest-globe');

    this.svg = svgEl('svg', {
      viewBox: '-100 -100 200 200',
      class: 'digest-globe-svg',
      'aria-hidden': 'true'
    });
    this.svg.appendChild(svgEl('circle', { cx: 0, cy: 0, r: RADIUS, class: 'digest-globe-ocean' }));

    this.graticule = svgEl('g', { class: 'digest-globe-graticule' });
    this.land = svgEl('g', { class: 'digest-globe-land' });
    this.svg.appendChild(this.graticule);
    this.svg.appendChild(this.land);
    this.svg.appendChild(svgEl('circle', { cx: 0, cy: 0, r: RADIUS, class: 'digest-globe-limb' }));
    container.appendChild(this.svg);

    this.markerLayer = document.createElement('div');
    this.markerLayer.className = 'digest-globe-markers';
    container.appendChild(this.markerLayer);

    this.landPaths = WORLD.map(function () {
      var path = svgEl('path', {});
      this.land.appendChild(path);
      return path;
    }, this);

    this.graticulePaths = [];
    for (var lon = -180; lon < 180; lon += 30) {
      this.graticulePaths.push({ path: this.graticule.appendChild(svgEl('path', {})), meridian: lon });
    }
    for (var lat = -60; lat <= 60; lat += 30) {
      this.graticulePaths.push({ path: this.graticule.appendChild(svgEl('path', {})), parallel: lat });
    }

    this.draw();
    this.start();
  }

  // Builds an SVG path from a lat/lon ring, breaking the path wherever the ring
  // crosses the limb so the far side of the globe isn't drawn through the near side.
  DigestGlobe.prototype._ringPath = function (ring) {
    var parts = [];
    var pen = false;
    for (var i = 0; i < ring.length; i++) {
      var point = project(ring[i][1], ring[i][0], this.lambda0);
      if (point.z <= 0) {
        pen = false;
        continue;
      }
      parts.push((pen ? 'L' : 'M') + point.x.toFixed(2) + ' ' + point.y.toFixed(2));
      pen = true;
    }
    return parts.join(' ');
  };

  DigestGlobe.prototype.draw = function () {
    this.landPaths.forEach(function (path, index) {
      path.setAttribute('d', this._ringPath(WORLD[index]));
    }, this);

    this.graticulePaths.forEach(function (entry) {
      var ring = [];
      var step;
      if (entry.meridian !== undefined) {
        for (step = -90; step <= 90; step += 5) { ring.push([entry.meridian, step]); }
      } else {
        for (step = -180; step <= 180; step += 5) { ring.push([step, entry.parallel]); }
      }
      entry.path.setAttribute('d', this._ringPath(ring));
    }, this);

    this.markers.forEach(function (marker) {
      var point = project(marker.lat, marker.lon, this.lambda0);
      if (point.z <= 0) {
        marker.el.style.opacity = '0';
        marker.el.style.pointerEvents = 'none';
        return;
      }
      marker.el.style.opacity = String(Math.min(1, 0.25 + point.z * 1.4));
      marker.el.style.pointerEvents = 'auto';
      marker.el.style.left = (50 + point.x / 2) + '%';
      marker.el.style.top = (50 + point.y / 2) + '%';
    }, this);
  };

  DigestGlobe.prototype.addMarker = function (lat, lon, options) {
    options = options || {};

    var el = document.createElement('span');
    el.className = 'digest-globe-marker';

    var icon = document.createElement('span');
    var iconName = ICONS[options.icon] ? options.icon : 'default';
    icon.className = 'digest-marker-icon digest-icon-' + iconName;
    icon.textContent = ICONS[iconName];
    if (options.color) {
      icon.style.color = options.color;
      icon.style.setProperty('--digest-glow-color', options.color);
    }
    // The glow class goes on the icon, not the wrapper: text-shadow is inherited
    // as an already-resolved value, so a var() set on a child cannot re-colour a
    // shadow declared on its parent.
    if (options.glow) {
      icon.classList.add('digest-glow');
    }
    el.appendChild(icon);

    if (options.label) {
      var label = document.createElement('span');
      label.className = 'digest-marker-label';
      label.textContent = options.label;
      el.appendChild(label);
      el.title = options.label;
    }

    this.markerLayer.appendChild(el);

    var marker = { lat: Number(lat) || 0, lon: Number(lon) || 0, el: el };
    this.markers.push(marker);
    this.draw();
    return marker;
  };

  DigestGlobe.prototype.start = function () {
    if (this.frame) { return; }
    if (global.matchMedia && global.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      return;
    }

    var self = this;
    var last = null;
    var tick = function (now) {
      if (last !== null) {
        self.lambda0 = (self.lambda0 + self.speed * (now - last) / 1000 + 180) % 360 - 180;
        self.draw();
      }
      last = now;
      self.frame = global.requestAnimationFrame(tick);
    };
    this.frame = global.requestAnimationFrame(tick);
  };

  DigestGlobe.prototype.stop = function () {
    if (this.frame) {
      global.cancelAnimationFrame(this.frame);
      this.frame = null;
    }
  };

  global.DigestGlobe = DigestGlobe;
})(window);
