import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import ClockRange from "../Core/ClockRange.js";
import ClockStep from "../Core/ClockStep.js";
import Color from "../Core/Color.js";
import createGuid from "../Core/createGuid.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Event from "../Core/Event.js";
import Iso8601 from "../Core/Iso8601.js";
import JulianDate from "../Core/JulianDate.js";
import NearFarScalar from "../Core/NearFarScalar.js";
import PinBuilder from "../Core/PinBuilder.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import TimeInterval from "../Core/TimeInterval.js";
import TimeIntervalCollection from "../Core/TimeIntervalCollection.js";
import HeightReference from "../Scene/HeightReference.js";
import HorizontalOrigin from "../Scene/HorizontalOrigin.js";
import LabelStyle from "../Scene/LabelStyle.js";
import VerticalOrigin from "../Scene/VerticalOrigin.js";
import Autolinker from "autolinker";
import BillboardGraphics from "./BillboardGraphics.js";
import ConstantProperty from "./ConstantProperty.js";
import DataSource from "./DataSource.js";
import DataSourceClock from "./DataSourceClock.js";
import EntityCluster from "./EntityCluster.js";
import EntityCollection from "./EntityCollection.js";
import LabelGraphics from "./LabelGraphics.js";
import PolylineGraphics from "./PolylineGraphics.js";
import PolylineOutlineMaterialProperty from "./PolylineOutlineMaterialProperty.js";
import SampledPositionProperty from "./SampledPositionProperty.js";

let parser;
if (typeof DOMParser !== "undefined") {
  parser = new DOMParser();
}

const autolinker = new Autolinker({
  stripPrefix: false,
  email: false,
  replaceFn: function (linker, match) {
    //Prevent matching of non-explicit urls.
    //i.e. foo.id won't match but http://foo.id will
    return match.urlMatchType === "scheme" || match.urlMatchType === "www";
  },
});

const BILLBOARD_SIZE = 32;
const BILLBOARD_NEAR_DISTANCE = 2414016;
const BILLBOARD_NEAR_RATIO = 1.0;
const BILLBOARD_FAR_DISTANCE = 1.6093e7;
const BILLBOARD_FAR_RATIO = 0.1;

const gpxNamespaces = [null, undefined, "http://www.topografix.com/GPX/1/1"];
const namespaces = {
  gpx: gpxNamespaces,
};

function readBlobAsText(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener("load", function () {
      resolve(reader.result);
    });
    reader.addEventListener("error", function () {
      reject(reader.error);
    });
    reader.readAsText(blob);
  });
}

function getOrCreateEntity(node, entityCollection) {
  let id = queryStringAttribute(node, "id");
  id = defined(id) ? id : createGuid();
  const entity = entityCollection.getOrCreateEntity(id);
  return entity;
}

function readCoordinateFromNode(node) {
  const longitude = queryNumericAttribute(node, "lon");
  const latitude = queryNumericAttribute(node, "lat");
  const elevation = queryNumericValue(node, "ele", namespaces.gpx);
  return Cartesian3.fromDegrees(longitude, latitude, elevation);
}

function queryNumericAttribute(node, attributeName) {
  if (!defined(node)) {
    return undefined;
  }

  const value = node.getAttribute(attributeName);
  if (value !== null) {
    const result = parseFloat(value);
    return !isNaN(result) ? result : undefined;
  }
  return undefined;
}

function queryStringAttribute(node, attributeName) {
  if (!defined(node)) {
    return undefined;
  }
  const value = node.getAttribute(attributeName);
  return value !== null ? value : undefined;
}

function queryFirstNode(node, tagName, namespace) {
  if (!defined(node)) {
    return undefined;
  }
  const childNodes = node.childNodes;
  const length = childNodes.length;
  for (let q = 0; q < length; q++) {
    const child = childNodes[q];
    if (
      child.localName === tagName &&
      namespace.indexOf(child.namespaceURI) !== -1
    ) {
      return child;
    }
  }
  return undefined;
}

function queryNodes(node, tagName, namespace) {
  if (!defined(node)) {
    return undefined;
  }
  const result = [];
  const childNodes = node.getElementsByTagName(tagName);
  const length = childNodes.length;
  for (let q = 0; q < length; q++) {
    const child = childNodes[q];
    if (
      child.localName === tagName &&
      namespace.indexOf(child.namespaceURI) !== -1
    ) {
      result.push(child);
    }
  }
  return result;
}

function queryNumericValue(node, tagName, namespace) {
  const resultNode = queryFirstNode(node, tagName, namespace);
  if (defined(resultNode)) {
    const result = parseFloat(resultNode.textContent);
    return !isNaN(result) ? result : undefined;
  }
  return undefined;
}

function queryStringValue(node, tagName, namespace) {
  const result = queryFirstNode(node, tagName, namespace);
  if (defined(result)) {
    return result.textContent.trim();
  }
  return undefined;
}

function createDefaultBillboard(image) {
  const billboard = new BillboardGraphics();
  billboard.width = BILLBOARD_SIZE;
  billboard.height = BILLBOARD_SIZE;
  billboard.scaleByDistance = new NearFarScalar(
    BILLBOARD_NEAR_DISTANCE,
    BILLBOARD_NEAR_RATIO,
    BILLBOARD_FAR_DISTANCE,
    BILLBOARD_FAR_RATIO,
  );
  billboard.pixelOffsetScaleByDistance = new NearFarScalar(
    BILLBOARD_NEAR_DISTANCE,
    BILLBOARD_NEAR_RATIO,
    BILLBOARD_FAR_DISTANCE,
    BILLBOARD_FAR_RATIO,
  );
  billboard.verticalOrigin = new ConstantProperty(VerticalOrigin.BOTTOM);
  billboard.image = image;
  return billboard;
}

function createDefaultLabel() {
  const label = new LabelGraphics();
  label.translucencyByDistance = new NearFarScalar(3000000, 1.0, 5000000, 0.0);
  label.pixelOffset = new Cartesian2(17, 0);
  label.horizontalOrigin = HorizontalOrigin.LEFT;
  label.font = "16px sans-serif";
  label.style = LabelStyle.FILL_AND_OUTLINE;
  return label;
}

function createDefaultPolyline(color) {
  const polyline = new PolylineGraphics();
  polyline.width = 4;
  polyline.material = new PolylineOutlineMaterialProperty();
  polyline.material.color = defined(color) ? color : Color.RED;
  polyline.material.outlineWidth = 2;
  polyline.material.outlineColor = Color.BLACK;
  return polyline;
}

// This is a list of the Optional Description Information:
//  <cmt> GPS comment of the waypoint
//  <desc> Descriptive description of the waypoint
//  <src> Source of the waypoint data
//  <type> Type (category) of waypoint
const descriptiveInfoTypes = {
  time: {
    text: "Time",
    tag: "time",
  },
  comment: {
    text: "Comment",
    tag: "cmt",
  },
  description: {
    text: "Description",
    tag: "desc",
  },
  source: {
    text: "Source",
    tag: "src",
  },
  number: {
    text: "GPS track/route number",
    tag: "number",
  },
  type: {
    text: "Type",
    tag: "type",
  },
};

let scratchDiv;
if (typeof document !== "undefined") {
  scratchDiv = document.createElement("div");
}
function processDescription(node, entity) {
  let i;

  let text = "";
  const infoTypeNames = Object.keys(descriptiveInfoTypes);
  const length = infoTypeNames.length;
  for (i = 0; i < length; i++) {
    const infoTypeName = infoTypeNames[i];
    const infoType = descriptiveInfoTypes[infoTypeName];
    infoType.value = queryStringValue(node, infoType.tag, namespaces.gpx) ?? "";
    if (defined(infoType.value) && infoType.value !== "") {
      text = `${text}<p>${infoType.text}: ${infoType.value}</p>`;
    }
  }

  if (!defined(text) || text === "") {
    // No description
    return;
  }

  // Turns non-explicit links into clickable links.
  text = autolinker.link(text);

  // Use a temporary div to manipulate the links
  // so that they open in a new window.
  scratchDiv.innerHTML = text;
  const links = scratchDiv.querySelectorAll("a");

  for (i = 0; i < links.length; i++) {
    links[i].setAttribute("target", "_blank");
  }

  const background = Color.WHITE;
  const foreground = Color.BLACK;
  let tmp = '<div class="cesium-infoBox-description-lighter" style="';
  tmp += "overflow:auto;";
  tmp += "word-wrap:break-word;";
  tmp += `background-color:${background.toCssColorString()};`;
  tmp += `color:${foreground.toCssColorString()};`;
  tmp += '">';
  tmp += `${scratchDiv.innerHTML}</div>`;
  scratchDiv.innerHTML = "";

  // return the final HTML as the description.
  return tmp;
}

function processWpt(dataSource, geometryNode, entityCollection, options) {
  const position = readCoordinateFromNode(geometryNode);

  const entity = getOrCreateEntity(geometryNode, entityCollection);
  entity.position = position;

  // Get billboard image
  const image = defined(options.waypointImage)
    ? options.waypointImage
    : dataSource._pinBuilder.fromMakiIconId(
        "marker",
        Color.RED,
        BILLBOARD_SIZE,
      );
  entity.billboard = createDefaultBillboard(image);

  const name = queryStringValue(geometryNode, "name", namespaces.gpx);
  entity.name = name;
  entity.label = createDefaultLabel();
  entity.label.text = name;
  entity.description = processDescription(geometryNode, entity);

  if (options.clampToGround) {
    entity.billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
    entity.label.heightReference = HeightReference.CLAMP_TO_GROUND;
  }
}

// rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination
function processRte(dataSource, geometryNode, entityCollection, options) {
  const entity = getOrCreateEntity(geometryNode, entityCollection);
  entity.description = processDescription(geometryNode, entity);

  // a list of waypoint
  const routePoints = queryNodes(geometryNode, "rtept", namespaces.gpx);
  const coordinateTuples = new Array(routePoints.length);
  for (let i = 0; i < routePoints.length; i++) {
    processWpt(dataSource, routePoints[i], entityCollection, options);
    coordinateTuples[i] = readCoordinateFromNode(routePoints[i]);
  }
  entity.polyline = createDefaultPolyline(options.routeColor);
  if (options.clampToGround) {
    entity.polyline.clampToGround = true;
  }
  entity.polyline.positions = coordinateTuples;
}

// trk represents a track - an ordered list of points describing a path.
function processTrk(dataSource, geometryNode, entityCollection, options) {
  const entity = getOrCreateEntity(geometryNode, entityCollection);
  entity.description = processDescription(geometryNode, entity);

  const trackSegs = queryNodes(geometryNode, "trkseg", namespaces.gpx);
  let positions = [];
  let times = [];
  let trackSegInfo;
  let isTimeDynamic = true;
  const property = new SampledPositionProperty();
  for (let i = 0; i < trackSegs.length; i++) {
    trackSegInfo = processTrkSeg(trackSegs[i]);
    positions = positions.concat(trackSegInfo.positions);
    if (trackSegInfo.times.length > 0) {
      times = times.concat(trackSegInfo.times);
      property.addSamples(times, positions);
      // if one track segment is non dynamic the whole track must also be
      isTimeDynamic = isTimeDynamic && true;
    } else {
      isTimeDynamic = false;
    }
  }
  if (isTimeDynamic) {
    // Assign billboard image
    const image = defined(options.waypointImage)
      ? options.waypointImage
      : dataSource._pinBuilder.fromMakiIconId(
          "marker",
          Color.RED,
          BILLBOARD_SIZE,
        );
    entity.billboard = createDefaultBillboard(image);
    entity.position = property;
    if (options.clampToGround) {
      entity.billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
    }
    entity.availability = new TimeIntervalCollection();
    entity.availability.addInterval(
      new TimeInterval({
        start: times[0],
        stop: times[times.length - 1],
      }),
    );
  }
  entity.polyline = createDefaultPolyline(options.trackColor);
  entity.polyline.positions = positions;
  if (options.clampToGround) {
    entity.polyline.clampToGround = true;
  }
}

function processTrkSeg(node) {
  const result = {
    positions: [],
    times: [],
  };
  const trackPoints = queryNodes(node, "trkpt", namespaces.gpx);
  let time;
  for (let i = 0; i < trackPoints.length; i++) {
    const position = readCoordinateFromNode(trackPoints[i]);
    result.positions.push(position);

    time = queryStringValue(trackPoints[i], "time", namespaces.gpx);
    if (defined(time)) {
      result.times.push(JulianDate.fromIso8601(time));
    }
  }
  return result;
}

// Processes a metadataType node and returns a metadata object
// {@link http://www.topografix.com/gpx/1/1/#type_metadataType|GPX Schema}
function processMetadata(node) {
  const metadataNode = queryFirstNode(node, "metadata", namespaces.gpx);
  if (defined(metadataNode)) {
    const metadata = {
      name: queryStringValue(metadataNode, "name", namespaces.gpx),
      desc: queryStringValue(metadataNode, "desc", namespaces.gpx),
      author: getPerson(metadataNode),
      copyright: getCopyright(metadataNode),
      link: getLink(metadataNode),
      time: queryStringValue(metadataNode, "time", namespaces.gpx),
      keywords: queryStringValue(metadataNode, "keywords", namespaces.gpx),
      bounds: getBounds(metadataNode),
    };
    if (
      defined(metadata.name) ||
      defined(metadata.desc) ||
      defined(metadata.author) ||
      defined(metadata.copyright) ||
      defined(metadata.link) ||
      defined(metadata.time) ||
      defined(metadata.keywords) ||
      defined(metadata.bounds)
    ) {
      return metadata;
    }
  }
  return undefined;
}

// Receives a XML node and returns a personType object, refer to
// {@link http://www.topografix.com/gpx/1/1/#type_personType|GPX Schema}
function getPerson(node) {
  const personNode = queryFirstNode(node, "author", namespaces.gpx);
  if (defined(personNode)) {
    const person = {
      name: queryStringValue(personNode, "name", namespaces.gpx),
      email: getEmail(personNode),
      link: getLink(personNode),
    };
    if (defined(person.name) || defined(person.email) || defined(person.link)) {
      return person;
    }
  }
  return undefined;
}

// Receives a XML node and returns an email address (from emailType), refer to
// {@link http://www.topografix.com/gpx/1/1/#type_emailType|GPX Schema}
function getEmail(node) {
  const emailNode = queryFirstNode(node, "email", namespaces.gpx);
  if (defined(emailNode)) {
    const id = queryStringValue(emailNode, "id", namespaces.gpx);
    const domain = queryStringValue(emailNode, "domain", namespaces.gpx);
    return `${id}@${domain}`;
  }
  return undefined;
}

// Receives a XML node and returns a linkType object, refer to
// {@link http://www.topografix.com/gpx/1/1/#type_linkType|GPX Schema}
function getLink(node) {
  const linkNode = queryFirstNode(node, "link", namespaces.gpx);
  if (defined(linkNode)) {
    const link = {
      href: queryStringAttribute(linkNode, "href"),
      text: queryStringValue(linkNode, "text", namespaces.gpx),
      mimeType: queryStringValue(linkNode, "type", namespaces.gpx),
    };
    if (defined(link.href) || defined(link.text) || defined(link.mimeType)) {
      return link;
    }
  }
  return undefined;
}

// Receives a XML node and returns a copyrightType object, refer to
// {@link http://www.topografix.com/gpx/1/1/#type_copyrightType|GPX Schema}
function getCopyright(node) {
  const copyrightNode = queryFirstNode(node, "copyright", namespaces.gpx);
  if (defined(copyrightNode)) {
    const copyright = {
      author: queryStringAttribute(copyrightNode, "author"),
      year: queryStringValue(copyrightNode, "year", namespaces.gpx),
      license: queryStringValue(copyrightNode, "license", namespaces.gpx),
    };
    if (
      defined(copyright.author) ||
      defined(copyright.year) ||
      defined(copyright.license)
    ) {
      return copyright;
    }
  }
  return undefined;
}

// Receives a XML node and returns a boundsType object, refer to
// {@link http://www.topografix.com/gpx/1/1/#type_boundsType|GPX Schema}
function getBounds(node) {
  const boundsNode = queryFirstNode(node, "bounds", namespaces.gpx);
  if (defined(boundsNode)) {
    const bounds = {
      minLat: queryNumericValue(boundsNode, "minlat", namespaces.gpx),
      maxLat: queryNumericValue(boundsNode, "maxlat", namespaces.gpx),
      minLon: queryNumericValue(boundsNode, "minlon", namespaces.gpx),
      maxLon: queryNumericValue(boundsNode, "maxlon", namespaces.gpx),
    };
    if (
      defined(bounds.minLat) ||
      defined(bounds.maxLat) ||
      defined(bounds.minLon) ||
      defined(bounds.maxLon)
    ) {
      return bounds;
    }
  }
  return undefined;
}

const complexTypes = {
  wpt: processWpt,
  rte: processRte,
  trk: processTrk,
};

function processGpx(dataSource, node, entityCollection, options) {
  const complexTypeNames = Object.keys(complexTypes);
  const complexTypeNamesLength = complexTypeNames.length;

  for (let i = 0; i < complexTypeNamesLength; i++) {
    const typeName = complexTypeNames[i];
    const processComplexTypeNode = complexTypes[typeName];

    const childNodes = node.childNodes;
    const length = childNodes.length;
    for (let q = 0; q < length; q++) {
      const child = childNodes[q];
      if (
        child.localName === typeName &&
        namespaces.gpx.indexOf(child.namespaceURI) !== -1
      ) {
        processComplexTypeNode(dataSource, child, entityCollection, options);
      }
    }
  }
}

function loadGpx(dataSource, gpx, options) {
  const entityCollection = dataSource._entityCollection;

  entityCollection.removeAll();

  const element = gpx.documentElement;
  const version = queryStringAttribute(element, "version");
  const creator = queryStringAttribute(element, "creator");

  let name;
  const metadata = processMetadata(element);
  if (defined(metadata)) {
    name = metadata.name;
  }

  if (element.localName === "gpx") {
    processGpx(dataSource, element, entityCollection, options);
  } else {
    console.log(`GPX - Unsupported node: ${element.localName}`);
  }

  let clock;
  const availability = entityCollection.computeAvailability();

  let start = availability.start;
  let stop = availability.stop;
  const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
  const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
  if (!isMinStart || !isMaxStop) {
    let date;

    // If start is min time just start at midnight this morning, local time
    if (isMinStart) {
      date = new Date();
      date.setHours(0, 0, 0, 0);
      start = JulianDate.fromDate(date);
    }

    // If stop is max value just stop at midnight tonight, local time
    if (isMaxStop) {
      date = new Date();
      date.setHours(24, 0, 0, 0);
      stop = JulianDate.fromDate(date);
    }

    clock = new DataSourceClock();
    clock.startTime = start;
    clock.stopTime = stop;
    clock.currentTime = JulianDate.clone(start);
    clock.clockRange = ClockRange.LOOP_STOP;
    clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
    clock.multiplier = Math.round(
      Math.min(
        Math.max(JulianDate.secondsDifference(stop, start) / 60, 1),
        3.15569e7,
      ),
    );
  }
  let changed = false;
  if (dataSource._name !== name) {
    dataSource._name = name;
    changed = true;
  }

  if (dataSource._creator !== creator) {
    dataSource._creator = creator;
    changed = true;
  }

  if (metadataChanged(dataSource._metadata, metadata)) {
    dataSource._metadata = metadata;
    changed = true;
  }

  if (dataSource._version !== version) {
    dataSource._version = version;
    changed = true;
  }

  if (clock !== dataSource._clock) {
    changed = true;
    dataSource._clock = clock;
  }

  if (changed) {
    dataSource._changed.raiseEvent(dataSource);
  }

  DataSource.setLoading(dataSource, false);
  return dataSource;
}

function metadataChanged(old, current) {
  if (!defined(old) && !defined(current)) {
    return false;
  } else if (defined(old) && defined(current)) {
    if (
      old.name !== current.name ||
      old.dec !== current.desc ||
      old.src !== current.src ||
      old.author !== current.author ||
      old.copyright !== current.copyright ||
      old.link !== current.link ||
      old.time !== current.time ||
      old.bounds !== current.bounds
    ) {
      return true;
    }
    return false;
  }
  return true;
}

function load(dataSource, entityCollection, data, options) {
  options = options ?? Frozen.EMPTY_OBJECT;
  let promise = data;
  if (typeof data === "string" || data instanceof Resource) {
    data = Resource.createIfNeeded(data);
    promise = data.fetchBlob();

    // Add resource credits to our list of credits to display
    const resourceCredits = dataSource._resourceCredits;
    const credits = data.credits;
    if (defined(credits)) {
      const length = credits.length;
      for (let i = 0; i < length; i++) {
        resourceCredits.push(credits[i]);
      }
    }
  }

  return Promise.resolve(promise)
    .then(function (dataToLoad) {
      if (dataToLoad instanceof Blob) {
        return readBlobAsText(dataToLoad).then(function (text) {
          // There's no official way to validate if a parse was successful.
          // The following check detects the error on various browsers.
          // IE raises an exception
          let gpx;
          let error;
          try {
            gpx = parser.parseFromString(text, "application/xml");
          } catch (e) {
            error = e.toString();
          }

          // The parse succeeds on Chrome and Firefox, but the error
          // handling is different in each.
          if (
            defined(error) ||
            gpx.body ||
            gpx.documentElement.tagName === "parsererror"
          ) {
            // Firefox has error information as the firstChild nodeValue.
            let msg = defined(error)
              ? error
              : gpx.documentElement.firstChild.nodeValue;

            // Chrome has it in the body text.
            if (!msg) {
              msg = gpx.body.innerText;
            }

            // Return the error
            throw new RuntimeError(msg);
          }
          return loadGpx(dataSource, gpx, options);
        });
      }
      return loadGpx(dataSource, dataToLoad, options);
    })
    .catch(function (error) {
      dataSource._error.raiseEvent(dataSource, error);
      console.log(error);
      return Promise.reject(error);
    });
}

/**
 * A {@link DataSource} which processes the GPS Exchange Format (GPX).
 *
 * @alias GpxDataSource
 * @constructor
 *
 * @see {@link http://www.topografix.com/gpx.asp|Topografix GPX Standard}
 * @see {@link http://www.topografix.com/gpx/1/1/|Topografix GPX Documentation}
 *
 * @demo {@link http://sandcastle.cesium.com/index.html?id=gpx}
 *
 * @example
 * const viewer = new Cesium.Viewer('cesiumContainer');
 * viewer.dataSources.add(Cesium.GpxDataSource.load('../../SampleData/track.gpx'));
 */
function GpxDataSource() {
  this._changed = new Event();
  this._error = new Event();
  this._loading = new Event();
  this._clock = undefined;
  this._entityCollection = new EntityCollection(this);
  this._entityCluster = new EntityCluster();
  this._name = undefined;
  this._version = undefined;
  this._creator = undefined;
  this._metadata = undefined;
  this._isLoading = false;
  this._pinBuilder = new PinBuilder();
}

/**
 * Creates a Promise to a new instance loaded with the provided GPX data.
 *
 * @param {string|Document|Blob} data A url, parsed GPX document, or Blob containing binary GPX data.
 * @param {object} [options] An object with the following properties:
 * @param {boolean} [options.clampToGround] True if the symbols should be rendered at the same height as the terrain
 * @param {string} [options.waypointImage] Image to use for waypoint billboards.
 * @param {string} [options.trackImage] Image to use for track billboards.
 * @param {string} [options.trackColor] Color to use for track lines.
 * @param {string} [options.routeColor] Color to use for route lines.
 * @returns {Promise<GpxDataSource>} A promise that will resolve to a new GpxDataSource instance once the gpx is loaded.
 */
GpxDataSource.load = function (data, options) {
  return new GpxDataSource().load(data, options);
};

Object.defineProperties(GpxDataSource.prototype, {
  /**
   * Gets a human-readable name for this instance.
   * This will be automatically be set to the GPX document name on load.
   * @memberof GpxDataSource.prototype
   * @type {string}
   */
  name: {
    get: function () {
      return this._name;
    },
  },
  /**
   * Gets the version of the GPX Schema in use.
   * @memberof GpxDataSource.prototype
   * @type {string}
   */
  version: {
    get: function () {
      return this._version;
    },
  },
  /**
   * Gets the creator of the GPX document.
   * @memberof GpxDataSource.prototype
   * @type {string}
   */
  creator: {
    get: function () {
      return this._creator;
    },
  },
  /**
   * Gets an object containing metadata about the GPX file.
   * @memberof GpxDataSource.prototype
   * @type {object}
   */
  metadata: {
    get: function () {
      return this._metadata;
    },
  },
  /**
   * Gets the clock settings defined by the loaded GPX. This represents the total
   * availability interval for all time-dynamic data. If the GPX does not contain
   * time-dynamic data, this value is undefined.
   * @memberof GpxDataSource.prototype
   * @type {DataSourceClock}
   */
  clock: {
    get: function () {
      return this._clock;
    },
  },
  /**
   * Gets the collection of {@link Entity} instances.
   * @memberof GpxDataSource.prototype
   * @type {EntityCollection}
   */
  entities: {
    get: function () {
      return this._entityCollection;
    },
  },
  /**
   * Gets a value indicating if the data source is currently loading data.
   * @memberof GpxDataSource.prototype
   * @type {boolean}
   */
  isLoading: {
    get: function () {
      return this._isLoading;
    },
  },
  /**
   * Gets an event that will be raised when the underlying data changes.
   * @memberof GpxDataSource.prototype
   * @type {Event}
   */
  changedEvent: {
    get: function () {
      return this._changed;
    },
  },
  /**
   * Gets an event that will be raised if an error is encountered during processing.
   * @memberof GpxDataSource.prototype
   * @type {Event}
   */
  errorEvent: {
    get: function () {
      return this._error;
    },
  },
  /**
   * Gets an event that will be raised when the data source either starts or stops loading.
   * @memberof GpxDataSource.prototype
   * @type {Event}
   */
  loadingEvent: {
    get: function () {
      return this._loading;
    },
  },
  /**
   * Gets whether or not this data source should be displayed.
   * @memberof GpxDataSource.prototype
   * @type {boolean}
   */
  show: {
    get: function () {
      return this._entityCollection.show;
    },
    set: function (value) {
      this._entityCollection.show = value;
    },
  },

  /**
   * Gets or sets the clustering options for this data source. This object can be shared between multiple data sources.
   *
   * @memberof GpxDataSource.prototype
   * @type {EntityCluster}
   */
  clustering: {
    get: function () {
      return this._entityCluster;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value must be defined.");
      }
      //>>includeEnd('debug');
      this._entityCluster = value;
    },
  },
});

/**
 * Updates the data source to the provided time.  This function is optional and
 * is not required to be implemented.  It is provided for data sources which
 * retrieve data based on the current animation time or scene state.
 * If implemented, update will be called by {@link DataSourceDisplay} once a frame.
 *
 * @param {JulianDate} time The simulation time.
 * @returns {boolean} True if this data source is ready to be displayed at the provided time, false otherwise.
 */
GpxDataSource.prototype.update = function (time) {
  return true;
};

/**
 * Asynchronously loads the provided GPX data, replacing any existing data.
 *
 * @param {string|Document|Blob} data A url, parsed GPX document, or Blob containing binary GPX data or a parsed GPX document.
 * @param {object} [options] An object with the following properties:
 * @param {boolean} [options.clampToGround] True if the symbols should be rendered at the same height as the terrain
 * @param {string} [options.waypointImage] Image to use for waypoint billboards.
 * @param {string} [options.trackImage] Image to use for track billboards.
 * @param {string} [options.trackColor] Color to use for track lines.
 * @param {string} [options.routeColor] Color to use for route lines.
 * @returns {Promise<GpxDataSource>} A promise that will resolve to this instances once the GPX is loaded.
 */
GpxDataSource.prototype.load = function (data, options) {
  if (!defined(data)) {
    throw new DeveloperError("data is required.");
  }

  options = options ?? Frozen.EMPTY_OBJECT;
  DataSource.setLoading(this, true);
  const oldName = this._name;
  const that = this;
  return load(this, this._entityCollection, data, options)
    .then(function () {
      let clock;

      const availability = that._entityCollection.computeAvailability();

      let start = availability.start;
      let stop = availability.stop;
      const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
      const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
      if (!isMinStart || !isMaxStop) {
        let date;

        // If start is min time just start at midnight this morning, local time
        if (isMinStart) {
          date = new Date();
          date.setHours(0, 0, 0, 0);
          start = JulianDate.fromDate(date);
        }

        // If stop is max value just stop at midnight tonight, local time
        if (isMaxStop) {
          date = new Date();
          date.setHours(24, 0, 0, 0);
          stop = JulianDate.fromDate(date);
        }

        clock = new DataSourceClock();
        clock.startTime = start;
        clock.stopTime = stop;
        clock.currentTime = JulianDate.clone(start);
        clock.clockRange = ClockRange.LOOP_STOP;
        clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
        clock.multiplier = Math.round(
          Math.min(
            Math.max(JulianDate.secondsDifference(stop, start) / 60, 1),
            3.15569e7,
          ),
        );
      }

      let changed = false;
      if (clock !== that._clock) {
        that._clock = clock;
        changed = true;
      }

      if (oldName !== that._name) {
        changed = true;
      }

      if (changed) {
        that._changed.raiseEvent(that);
      }

      DataSource.setLoading(that, false);

      return that;
    })
    .catch(function (error) {
      DataSource.setLoading(that, false);
      that._error.raiseEvent(that, error);
      console.log(error);
      return Promise.reject(error);
    });
};

export default GpxDataSource;
