<script>
  import * as d3 from "d3";
  import * as sjs from "satellite.js";
  import { onMount } from "svelte";
  import tles from "../../static/tle.js";
  import world110m from "../../static/world-110m.js";
  import * as topojson from "topojson";

  export let width = 300;

  async function doThing() {
    let satelliteJs = sjs;
    var RADIANS = Math.PI / 180;
    var DEGREES = 180 / Math.PI;
    var R_EARTH = 6378.137; // equatorial radius (km)

    /* =============================================== */
    /* =============== CLOCK ========================= */
    /* =============================================== */

    /**
     * Factory function for keeping track of elapsed time and rates.
     */
    class Clock {
      constructor() {
        this._rate = 60; // 1ms elapsed : 60sec simulated
        this._date = d3.now();
        this._elapsed = 0;
      }
      async date(timeInMs) {
        if (!arguments.length) return this._date + this._elapsed * this._rate;
        this._date = timeInMs;
        return this;
      }
      async elapsed(ms) {
        if (!arguments.length) return this._date - d3.now(); // calculates elapsed
        this._elapsed = ms;
        return this;
      }
      async rate(secondsPerMsElapsed) {
        if (!arguments.length) return this._rate;
        this._rate = secondsPerMsElapsed;
        return this;
      }
    }

    /* ==================================================== */
    /* =============== CONVERSION ========================= */
    /* ==================================================== */

    async function satrecToFeature(satrec, date, props) {
      var properties = props || {};
      var positionAndVelocity = satelliteJs.propagate(satrec, date);
      var gmst = satelliteJs.gstime(date);
      var positionGd = satelliteJs.eciToGeodetic(
        positionAndVelocity.position,
        gmst
      );
      properties.height = positionGd.height;
      return {
        type: "Feature",
        properties: properties,
        geometry: {
          type: "Point",
          coordinates: [
            positionGd.longitude * DEGREES,
            positionGd.latitude * DEGREES,
          ],
        },
      };
    }
    /* ==================================================== */
    /* =============== TLE ================================ */
    /* ==================================================== */

    /**
     * Factory function for working with TLE.
     */
    class TLE {
      constructor() {
        this._properties;
        this._date;
      }
      async _lines(arry) {
        return arry.slice(0, 2);
      }
      async satrecs(tles) {
        return tles.map(function (d) {
          return satelliteJs.twoline2satrec.apply(null, this._lines(d));
        });
      }
      async features(tles) {
        var date = this._date || d3.now();

        return tles.map(function (d) {
          var satrec = satelliteJs.twoline2satrec.apply(null, this._lines(d));
          return satrecToFeature(satrec, date, this._properties(d));
        });
      }
      async lines(func) {
        if (!arguments.length) return this._lines;
        this._lines = func;
        return this;
      }
      async properties(func) {
        if (!arguments.length) return this._properties;
        this._properties = func;
        return this;
      }
      async date(ms) {
        if (!arguments.length) return this._date;
        this._date = ms;
        return this;
      }
    }

    /* ==================================================== */
    /* =============== PARSE ============================== */
    /* ==================================================== */

    /**
     * Parses text file string of tle into groups.
     * @return {string[][]} Like [['tle line 1', 'tle line 2'], ...]
     */
    async function parseTle(tleString) {
      // remove last newline so that we can properly split all the lines
      var lines = tleString.replace(/\r?\n$/g, "").split(/\r?\n/);

      return lines.reduce(function (acc, cur, index) {
        if (index % 2 === 0) acc.push([]);
        acc[acc.length - 1].push(cur);
        return acc;
      }, []);
    }

    /* ==================================================== */
    /* =============== SATELLITE ========================== */
    /* ==================================================== */

    /**
     * Satellite factory function that wraps satellitejs functionality
     * and can compute footprints based on TLE and date
     *
     * @param {string[][]} tle two-line element
     * @param {Date} date date to propagate with TLE
     */
    function Satellite(tle, date) {
      this._satrec = satelliteJs.twoline2satrec(tle[0], tle[1]);
      this._satNum = this._satrec.satnum; // NORAD Catalog Number

      this._altitude; // km
      this._position = {
        lat: null,
        lng: null,
      };
      this._halfAngle; // degrees
      this._date;
      this._gmst;

      this.setDate(date);
      this.update();
      this._orbitType = this.orbitTypeFromAlt(this._altitude); // LEO, MEO, or GEO
    }

    /**
     * Updates satellite position and altitude based on current TLE and date
     */
    Satellite.prototype.update = async function () {
      var positionAndVelocity = satelliteJs.propagate(this._satrec, this._date);
      var positionGd = satelliteJs.eciToGeodetic(
        positionAndVelocity.position,
        this._gmst
      );

      this._position = {
        lat: positionGd.latitude * DEGREES,
        lng: positionGd.longitude * DEGREES,
      };
      this._altitude = positionGd.height;
      return this;
    };

    /**
     * @returns {GeoJSON.Polygon} GeoJSON describing the satellite's current footprint on the Earth
     */
    Satellite.prototype.getFootprint = function () {
      var theta = this._halfAngle * RADIANS;

      let coreAngle = this._coreAngle(theta, this._altitude, R_EARTH) * DEGREES;

      return d3
        .geoCircle()
        .center([this._position.lng, this._position.lat])
        .radius(coreAngle)();
    };

    /**
     * A conical satellite with half angle casts a circle on the Earth. Find the angle
     * from the center of the earth to the radius of this circle
     * @param {number} theta: Satellite half angle in radians
     * @param {number} altitude Satellite altitude
     * @param {number} r Earth radius
     * @returns {number} core angle in radians
     */
    Satellite.prototype._coreAngle = function (theta, altitude, r) {
      // if FOV is larger than Earth, assume it goes to the tangential point
      if (Math.sin(theta) > r / (altitude + r)) {
        return Math.acos(r / (r + altitude));
      }
      return (
        Math.abs(Math.asin(((r + altitude) * Math.sin(theta)) / r)) - theta
      );
    };

    Satellite.prototype.halfAngle = function (halfAngle) {
      if (!arguments.length) return this._halfAngle;
      this._halfAngle = halfAngle;
      return this;
    };

    Satellite.prototype.satNum = function (satNum) {
      if (!arguments.length) return this._satNum;
      this._satNum = satNum;
      return this;
    };

    Satellite.prototype.altitude = function (altitude) {
      if (!arguments.length) return this._altitude;
      this._altitude = altitude;
      return this;
    };

    Satellite.prototype.position = function (position) {
      if (!arguments.length) return this._position;
      this._position = position;
      return this;
    };

    Satellite.prototype.getOrbitType = function () {
      return this._orbitType;
    };

    /**
     * sets both the date and the Greenwich Mean Sidereal Time
     * @param {Date} date
     */
    Satellite.prototype.setDate = function (date) {
      this._date = date;
      this._gmst = satelliteJs.gstime(date);
      return this;
    };

    /**
     * Maps an altitude to a type of satellite
     * @param {number} altitude (in KM)
     * @returns {'LEO' | 'MEO' | 'GEO'}
     */
    Satellite.prototype.orbitTypeFromAlt = function (altitude) {
      this._altitude = altitude || this._altitude;
      return this._altitude < 1200
        ? "LEO"
        : this._altitude > 22000
        ? "GEO"
        : "MEO";
    };

    /* =============================================== */
    /* =================== GLOBE ===================== */
    /* =============================================== */

    // Approximate date the tle data was aquired from https://www.space-track.org/#recent
    var TLE_DATA_DATE = new Date(2015, 11, 3, 17, 36).getTime();

    var activeClock;
    var sats;

    var svg = d3.select("#globe");

    var marginTop = 0;
    var width = svg.attr("width");
    var height = svg.attr("height") - marginTop;

    var projection = d3
      .geoOrthographic()
      .scale((height - 10) / 2)
      .translate([width / 2, height / 2 + marginTop])
      .rotate([45, -30]);

    var geoPath = d3.geoPath().projection(projection);

    svg
      .append("path")
      .datum({
        type: "Sphere",
      })
      .style("cursor", "grab")
      .attr("fill", "#2E86AB")
      .attr("d", geoPath);

    function initGlobe() {
      let worldData = world110m;
      svg
        .selectAll(".segment")
        .data([topojson.feature(worldData, worldData.objects.land)])
        .enter()
        .append("path")
        .style("cursor", "grab")
        .attr("class", "segment")
        .attr("d", geoPath)
        .style("stroke", "#88888800")
        .style("stroke-width", "0px")
        .style("fill", "#ffffff")
        .style("opacity", "1");
    }

    async function updateSats(date) {
      sats.forEach(async function (sat) {
        return sat.setDate(date).update();
      });
      return sats;
    }

    /**
     * Create satellite objects for each record in the TLEs and begin animation
     * @param {string[][]} parsedTles
     */
    async function initSats(parsedTles) {
      activeClock = new Clock();

      await activeClock.rate(100);
      await activeClock.date(TLE_DATA_DATE);

      sats = await Promise.all(
        parsedTles.map(async function (tle) {
          var sat = new Satellite(tle, new Date(2015, 11, 3, 17, 36));
          sat.halfAngle(61.73);
          return sat;
        })
      );

      window.requestAnimationFrame(animateSats);
      return sats;
    }

    async function draw() {
      // redrawGlobe();
      let allFootprints = svg.selectAll(".footprint");

      let allFootprintsData = allFootprints.data(sats, async function (sat) {
        return sat.satNum();
      });

      let allFootprintsDataJoin = allFootprintsData.join(function (enter) {
        return enter
          .append("path")
          .attr("class", function (sat) {
            return "footprint footprint--" + sat.getOrbitType();
          })
          .style("cursor", "grab");
      });

      allFootprintsDataJoin.attr("d", function (sat) {
        return geoPath(sat.getFootprint());
      });
    }

    async function redrawGlobe() {
      let allSelectedSegment = svg.selectAll(".segment");
      allSelectedSegment.attr("d", geoPath);
    }

    var m0;
    var o0;

    async function mousedown(e) {
      m0 = [e.pageX, e.pageY];
      o0 = projection.rotate();
      e.preventDefault();
    }

    async function mousemove(e) {
      if (m0) {
        var m1 = [e.pageX, e.pageY];
        const o1 = [
          o0[0] + (m1[0] - m0[0]) / 2.5,
          o0[1] + (m0[1] - m1[1]) / 2.5,
        ];
        projection.rotate(o1);
        redrawGlobe();
      }
    }

    async function mouseup(e) {
      if (m0) {
        mousemove(e);
        m0 = null;
      }
    }

    svg.on("mousedown", mousedown);
    d3.select(window).on("mousemove", mousemove).on("mouseup", mouseup);

    async function animateSats(elapsed) {
      var dateInMsI1 = await activeClock.elapsed(elapsed);
      var dateInMs = dateInMsI1.date();
      var date = new Date(await dateInMs);

      updateSats(date);
      draw();
      window.requestAnimationFrame(animateSats);
    }

    initGlobe();

    await initSats(await parseTle(tles));
  }

  onMount(async () => {
    doThing();
  });
</script>

<div class="globeContainer">
  <svg id="globe" {width} height={width} />
  <div class="globeText">click and drag</div>
</div>

<style>
  .globeContainer {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    background-color: #00111d;
    color: #ffffff;
  }

  :global(.footprint--LEO) {
    fill: rgba(255, 177, 64, 0.08);
    stroke: rgba(255, 177, 64, 0.5);
  }

  .globeText {
    text-align: center;
    font-size: 0.6em;
    color: #aaaaaa;
  }

  #globe {
    display: block;
  }
</style>
