import ArcType from "../Core/ArcType.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Color from "../Core/Color.js";
import createGuid from "../Core/createGuid.js";
import Credit from "../Core/Credit.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Event from "../Core/Event.js";
import getFilenameFromUri from "../Core/getFilenameFromUri.js";
import PinBuilder from "../Core/PinBuilder.js";
import PolygonHierarchy from "../Core/PolygonHierarchy.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import HeightReference from "../Scene/HeightReference.js";
import VerticalOrigin from "../Scene/VerticalOrigin.js";
import topojson from "../ThirdParty/topojson.js";
import when from "../ThirdParty/when.js";
import BillboardGraphics from "./BillboardGraphics.js";
import CallbackProperty from "./CallbackProperty.js";
import ColorMaterialProperty from "./ColorMaterialProperty.js";
import ConstantPositionProperty from "./ConstantPositionProperty.js";
import ConstantProperty from "./ConstantProperty.js";
import DataSource from "./DataSource.js";
import EntityCluster from "./EntityCluster.js";
import EntityCollection from "./EntityCollection.js";
import PolygonGraphics from "./PolygonGraphics.js";
import PolylineGraphics from "./PolylineGraphics.js";

function defaultCrsFunction(coordinates) {
  return Cartesian3.fromDegrees(coordinates[0], coordinates[1], coordinates[2]);
}

var crsNames = {
  "urn:ogc:def:crs:OGC:1.3:CRS84": defaultCrsFunction,
  "EPSG:4326": defaultCrsFunction,
  "urn:ogc:def:crs:EPSG::4326": defaultCrsFunction,
};

var crsLinkHrefs = {};
var crsLinkTypes = {};
var defaultMarkerSize = 48;
var defaultMarkerSymbol;
var defaultMarkerColor = Color.ROYALBLUE;
var defaultStroke = Color.YELLOW;
var defaultStrokeWidth = 2;
var defaultFill = Color.fromBytes(255, 255, 0, 100);
var defaultClampToGround = false;

var sizes = {
  small: 24,
  medium: 48,
  large: 64,
};

var simpleStyleIdentifiers = [
  "title",
  "description", //
  "marker-size",
  "marker-symbol",
  "marker-color",
  "stroke", //
  "stroke-opacity",
  "stroke-width",
  "fill",
  "fill-opacity",
];

function defaultDescribe(properties, nameProperty) {
  var html = "";
  for (var key in properties) {
    if (properties.hasOwnProperty(key)) {
      if (key === nameProperty || simpleStyleIdentifiers.indexOf(key) !== -1) {
        continue;
      }
      var value = properties[key];
      if (defined(value)) {
        if (typeof value === "object") {
          html +=
            "<tr><th>" +
            key +
            "</th><td>" +
            defaultDescribe(value) +
            "</td></tr>";
        } else {
          html += "<tr><th>" + key + "</th><td>" + value + "</td></tr>";
        }
      }
    }
  }

  if (html.length > 0) {
    html =
      '<table class="cesium-infoBox-defaultTable"><tbody>' +
      html +
      "</tbody></table>";
  }

  return html;
}

function createDescriptionCallback(describe, properties, nameProperty) {
  var description;
  return function (time, result) {
    if (!defined(description)) {
      description = describe(properties, nameProperty);
    }
    return description;
  };
}

function defaultDescribeProperty(properties, nameProperty) {
  return new CallbackProperty(
    createDescriptionCallback(defaultDescribe, properties, nameProperty),
    true
  );
}

//GeoJSON specifies only the Feature object has a usable id property
//But since "multi" geometries create multiple entity,
//we can't use it for them either.
function createObject(geoJson, entityCollection, describe) {
  var id = geoJson.id;
  if (!defined(id) || geoJson.type !== "Feature") {
    id = createGuid();
  } else {
    var i = 2;
    var finalId = id;
    while (defined(entityCollection.getById(finalId))) {
      finalId = id + "_" + i;
      i++;
    }
    id = finalId;
  }

  var entity = entityCollection.getOrCreateEntity(id);
  var properties = geoJson.properties;
  if (defined(properties)) {
    entity.properties = properties;

    var nameProperty;

    //Check for the simplestyle specified name first.
    var name = properties.title;
    if (defined(name)) {
      entity.name = name;
      nameProperty = "title";
    } else {
      //Else, find the name by selecting an appropriate property.
      //The name will be obtained based on this order:
      //1) The first case-insensitive property with the name 'title',
      //2) The first case-insensitive property with the name 'name',
      //3) The first property containing the word 'title'.
      //4) The first property containing the word 'name',
      var namePropertyPrecedence = Number.MAX_VALUE;
      for (var key in properties) {
        if (properties.hasOwnProperty(key) && properties[key]) {
          var lowerKey = key.toLowerCase();

          if (namePropertyPrecedence > 1 && lowerKey === "title") {
            namePropertyPrecedence = 1;
            nameProperty = key;
            break;
          } else if (namePropertyPrecedence > 2 && lowerKey === "name") {
            namePropertyPrecedence = 2;
            nameProperty = key;
          } else if (namePropertyPrecedence > 3 && /title/i.test(key)) {
            namePropertyPrecedence = 3;
            nameProperty = key;
          } else if (namePropertyPrecedence > 4 && /name/i.test(key)) {
            namePropertyPrecedence = 4;
            nameProperty = key;
          }
        }
      }
      if (defined(nameProperty)) {
        entity.name = properties[nameProperty];
      }
    }

    var description = properties.description;
    if (description !== null) {
      entity.description = !defined(description)
        ? describe(properties, nameProperty)
        : new ConstantProperty(description);
    }
  }
  return entity;
}

function coordinatesArrayToCartesianArray(coordinates, crsFunction) {
  var positions = new Array(coordinates.length);
  for (var i = 0; i < coordinates.length; i++) {
    positions[i] = crsFunction(coordinates[i]);
  }
  return positions;
}

var geoJsonObjectTypes = {
  Feature: processFeature,
  FeatureCollection: processFeatureCollection,
  GeometryCollection: processGeometryCollection,
  LineString: processLineString,
  MultiLineString: processMultiLineString,
  MultiPoint: processMultiPoint,
  MultiPolygon: processMultiPolygon,
  Point: processPoint,
  Polygon: processPolygon,
  Topology: processTopology,
};

var geometryTypes = {
  GeometryCollection: processGeometryCollection,
  LineString: processLineString,
  MultiLineString: processMultiLineString,
  MultiPoint: processMultiPoint,
  MultiPolygon: processMultiPolygon,
  Point: processPoint,
  Polygon: processPolygon,
  Topology: processTopology,
};

// GeoJSON processing functions
function processFeature(dataSource, feature, notUsed, crsFunction, options) {
  if (feature.geometry === null) {
    //Null geometry is allowed, so just create an empty entity instance for it.
    createObject(feature, dataSource._entityCollection, options.describe);
    return;
  }

  if (!defined(feature.geometry)) {
    throw new RuntimeError("feature.geometry is required.");
  }

  var geometryType = feature.geometry.type;
  var geometryHandler = geometryTypes[geometryType];
  if (!defined(geometryHandler)) {
    throw new RuntimeError("Unknown geometry type: " + geometryType);
  }
  geometryHandler(dataSource, feature, feature.geometry, crsFunction, options);
}

function processFeatureCollection(
  dataSource,
  featureCollection,
  notUsed,
  crsFunction,
  options
) {
  var features = featureCollection.features;
  for (var i = 0, len = features.length; i < len; i++) {
    processFeature(dataSource, features[i], undefined, crsFunction, options);
  }
}

function processGeometryCollection(
  dataSource,
  geoJson,
  geometryCollection,
  crsFunction,
  options
) {
  var geometries = geometryCollection.geometries;
  for (var i = 0, len = geometries.length; i < len; i++) {
    var geometry = geometries[i];
    var geometryType = geometry.type;
    var geometryHandler = geometryTypes[geometryType];
    if (!defined(geometryHandler)) {
      throw new RuntimeError("Unknown geometry type: " + geometryType);
    }
    geometryHandler(dataSource, geoJson, geometry, crsFunction, options);
  }
}

function createPoint(dataSource, geoJson, crsFunction, coordinates, options) {
  var symbol = options.markerSymbol;
  var color = options.markerColor;
  var size = options.markerSize;

  var properties = geoJson.properties;
  if (defined(properties)) {
    var cssColor = properties["marker-color"];
    if (defined(cssColor)) {
      color = Color.fromCssColorString(cssColor);
    }

    size = defaultValue(sizes[properties["marker-size"]], size);
    var markerSymbol = properties["marker-symbol"];
    if (defined(markerSymbol)) {
      symbol = markerSymbol;
    }
  }

  var canvasOrPromise;
  if (defined(symbol)) {
    if (symbol.length === 1) {
      canvasOrPromise = dataSource._pinBuilder.fromText(
        symbol.toUpperCase(),
        color,
        size
      );
    } else {
      canvasOrPromise = dataSource._pinBuilder.fromMakiIconId(
        symbol,
        color,
        size
      );
    }
  } else {
    canvasOrPromise = dataSource._pinBuilder.fromColor(color, size);
  }

  var billboard = new BillboardGraphics();
  billboard.verticalOrigin = new ConstantProperty(VerticalOrigin.BOTTOM);

  // Clamp to ground if there isn't a height specified
  if (coordinates.length === 2 && options.clampToGround) {
    billboard.heightReference = HeightReference.CLAMP_TO_GROUND;
  }

  var entity = createObject(
    geoJson,
    dataSource._entityCollection,
    options.describe
  );
  entity.billboard = billboard;
  entity.position = new ConstantPositionProperty(crsFunction(coordinates));

  var promise = when(canvasOrPromise)
    .then(function (image) {
      billboard.image = new ConstantProperty(image);
    })
    .otherwise(function () {
      billboard.image = new ConstantProperty(
        dataSource._pinBuilder.fromColor(color, size)
      );
    });

  dataSource._promises.push(promise);
}

function processPoint(dataSource, geoJson, geometry, crsFunction, options) {
  createPoint(dataSource, geoJson, crsFunction, geometry.coordinates, options);
}

function processMultiPoint(
  dataSource,
  geoJson,
  geometry,
  crsFunction,
  options
) {
  var coordinates = geometry.coordinates;
  for (var i = 0; i < coordinates.length; i++) {
    createPoint(dataSource, geoJson, crsFunction, coordinates[i], options);
  }
}

function createLineString(
  dataSource,
  geoJson,
  crsFunction,
  coordinates,
  options
) {
  var material = options.strokeMaterialProperty;
  var widthProperty = options.strokeWidthProperty;

  var properties = geoJson.properties;
  if (defined(properties)) {
    var width = properties["stroke-width"];
    if (defined(width)) {
      widthProperty = new ConstantProperty(width);
    }

    var color;
    var stroke = properties.stroke;
    if (defined(stroke)) {
      color = Color.fromCssColorString(stroke);
    }
    var opacity = properties["stroke-opacity"];
    if (defined(opacity) && opacity !== 1.0) {
      if (!defined(color)) {
        color = material.color.clone();
      }
      color.alpha = opacity;
    }
    if (defined(color)) {
      material = new ColorMaterialProperty(color);
    }
  }

  var entity = createObject(
    geoJson,
    dataSource._entityCollection,
    options.describe
  );
  var polylineGraphics = new PolylineGraphics();
  entity.polyline = polylineGraphics;

  polylineGraphics.clampToGround = options.clampToGround;
  polylineGraphics.material = material;
  polylineGraphics.width = widthProperty;
  polylineGraphics.positions = new ConstantProperty(
    coordinatesArrayToCartesianArray(coordinates, crsFunction)
  );
  polylineGraphics.arcType = ArcType.RHUMB;
}

function processLineString(
  dataSource,
  geoJson,
  geometry,
  crsFunction,
  options
) {
  createLineString(
    dataSource,
    geoJson,
    crsFunction,
    geometry.coordinates,
    options
  );
}

function processMultiLineString(
  dataSource,
  geoJson,
  geometry,
  crsFunction,
  options
) {
  var lineStrings = geometry.coordinates;
  for (var i = 0; i < lineStrings.length; i++) {
    createLineString(dataSource, geoJson, crsFunction, lineStrings[i], options);
  }
}

function createPolygon(dataSource, geoJson, crsFunction, coordinates, options) {
  if (coordinates.length === 0 || coordinates[0].length === 0) {
    return;
  }

  var outlineColorProperty = options.strokeMaterialProperty.color;
  var material = options.fillMaterialProperty;
  var widthProperty = options.strokeWidthProperty;

  var properties = geoJson.properties;
  if (defined(properties)) {
    var width = properties["stroke-width"];
    if (defined(width)) {
      widthProperty = new ConstantProperty(width);
    }

    var color;
    var stroke = properties.stroke;
    if (defined(stroke)) {
      color = Color.fromCssColorString(stroke);
    }
    var opacity = properties["stroke-opacity"];
    if (defined(opacity) && opacity !== 1.0) {
      if (!defined(color)) {
        color = options.strokeMaterialProperty.color.clone();
      }
      color.alpha = opacity;
    }

    if (defined(color)) {
      outlineColorProperty = new ConstantProperty(color);
    }

    var fillColor;
    var fill = properties.fill;
    if (defined(fill)) {
      fillColor = Color.fromCssColorString(fill);
      fillColor.alpha = material.color.alpha;
    }
    opacity = properties["fill-opacity"];
    if (defined(opacity) && opacity !== material.color.alpha) {
      if (!defined(fillColor)) {
        fillColor = material.color.clone();
      }
      fillColor.alpha = opacity;
    }
    if (defined(fillColor)) {
      material = new ColorMaterialProperty(fillColor);
    }
  }

  var polygon = new PolygonGraphics();
  polygon.outline = new ConstantProperty(true);
  polygon.outlineColor = outlineColorProperty;
  polygon.outlineWidth = widthProperty;
  polygon.material = material;
  polygon.arcType = ArcType.RHUMB;

  var holes = [];
  for (var i = 1, len = coordinates.length; i < len; i++) {
    holes.push(
      new PolygonHierarchy(
        coordinatesArrayToCartesianArray(coordinates[i], crsFunction)
      )
    );
  }

  var positions = coordinates[0];
  polygon.hierarchy = new ConstantProperty(
    new PolygonHierarchy(
      coordinatesArrayToCartesianArray(positions, crsFunction),
      holes
    )
  );
  if (positions[0].length > 2) {
    polygon.perPositionHeight = new ConstantProperty(true);
  } else if (!options.clampToGround) {
    polygon.height = 0;
  }

  var entity = createObject(
    geoJson,
    dataSource._entityCollection,
    options.describe
  );
  entity.polygon = polygon;
}

function processPolygon(dataSource, geoJson, geometry, crsFunction, options) {
  createPolygon(
    dataSource,
    geoJson,
    crsFunction,
    geometry.coordinates,
    options
  );
}

function processMultiPolygon(
  dataSource,
  geoJson,
  geometry,
  crsFunction,
  options
) {
  var polygons = geometry.coordinates;
  for (var i = 0; i < polygons.length; i++) {
    createPolygon(dataSource, geoJson, crsFunction, polygons[i], options);
  }
}

function processTopology(dataSource, geoJson, geometry, crsFunction, options) {
  for (var property in geometry.objects) {
    if (geometry.objects.hasOwnProperty(property)) {
      var feature = topojson.feature(geometry, geometry.objects[property]);
      var typeHandler = geoJsonObjectTypes[feature.type];
      typeHandler(dataSource, feature, feature, crsFunction, options);
    }
  }
}

/**
 * @typedef {Object} GeoJsonDataSource.LoadOptions
 *
 * Initialization options for the `load` method.
 *
 * @property {String} [sourceUri] Overrides the url to use for resolving relative links.
 * @property {Number} [markerSize=GeoJsonDataSource.markerSize] The default size of the map pin created for each point, in pixels.
 * @property {String} [markerSymbol=GeoJsonDataSource.markerSymbol] The default symbol of the map pin created for each point.
 * @property {Color} [markerColor=GeoJsonDataSource.markerColor] The default color of the map pin created for each point.
 * @property {Color} [stroke=GeoJsonDataSource.stroke] The default color of polylines and polygon outlines.
 * @property {Number} [strokeWidth=GeoJsonDataSource.strokeWidth] The default width of polylines and polygon outlines.
 * @property {Color} [fill=GeoJsonDataSource.fill] The default color for polygon interiors.
 * @property {Boolean} [clampToGround=GeoJsonDataSource.clampToGround] true if we want the geometry features (polygons or linestrings) clamped to the ground.
 * @property {Credit|String} [credit] A credit for the data source, which is displayed on the canvas.
 */

/**
 * A {@link DataSource} which processes both
 * {@link http://www.geojson.org/|GeoJSON} and {@link https://github.com/mbostock/topojson|TopoJSON} data.
 * {@link https://github.com/mapbox/simplestyle-spec|simplestyle-spec} properties will also be used if they
 * are present.
 *
 * @alias GeoJsonDataSource
 * @constructor
 *
 * @param {String} [name] The name of this data source.  If undefined, a name will be taken from
 *                        the name of the GeoJSON file.
 *
 * @demo {@link https://sandcastle.cesium.com/index.html?src=GeoJSON%20and%20TopoJSON.html|Cesium Sandcastle GeoJSON and TopoJSON Demo}
 * @demo {@link https://sandcastle.cesium.com/index.html?src=GeoJSON%20simplestyle.html|Cesium Sandcastle GeoJSON simplestyle Demo}
 *
 * @example
 * var viewer = new Cesium.Viewer('cesiumContainer');
 * viewer.dataSources.add(Cesium.GeoJsonDataSource.load('../../SampleData/ne_10m_us_states.topojson', {
 *   stroke: Cesium.Color.HOTPINK,
 *   fill: Cesium.Color.PINK,
 *   strokeWidth: 3,
 *   markerSymbol: '?'
 * }));
 */
function GeoJsonDataSource(name) {
  this._name = name;
  this._changed = new Event();
  this._error = new Event();
  this._isLoading = false;
  this._loading = new Event();
  this._entityCollection = new EntityCollection(this);
  this._promises = [];
  this._pinBuilder = new PinBuilder();
  this._entityCluster = new EntityCluster();
  this._credit = undefined;
  this._resourceCredits = [];
}

/**
 * Creates a Promise to a new instance loaded with the provided GeoJSON or TopoJSON data.
 *
 * @param {Resource|String|Object} data A url, GeoJSON object, or TopoJSON object to be loaded.
 * @param {GeoJsonDataSource.LoadOptions} [options] An object specifying configuration options
 *
 * @returns {Promise.<GeoJsonDataSource>} A promise that will resolve when the data is loaded.
 */
GeoJsonDataSource.load = function (data, options) {
  return new GeoJsonDataSource().load(data, options);
};

Object.defineProperties(GeoJsonDataSource, {
  /**
   * Gets or sets the default size of the map pin created for each point, in pixels.
   * @memberof GeoJsonDataSource
   * @type {Number}
   * @default 48
   */
  markerSize: {
    get: function () {
      return defaultMarkerSize;
    },
    set: function (value) {
      defaultMarkerSize = value;
    },
  },
  /**
   * Gets or sets the default symbol of the map pin created for each point.
   * This can be any valid {@link http://mapbox.com/maki/|Maki} identifier, any single character,
   * or blank if no symbol is to be used.
   * @memberof GeoJsonDataSource
   * @type {String}
   */
  markerSymbol: {
    get: function () {
      return defaultMarkerSymbol;
    },
    set: function (value) {
      defaultMarkerSymbol = value;
    },
  },
  /**
   * Gets or sets the default color of the map pin created for each point.
   * @memberof GeoJsonDataSource
   * @type {Color}
   * @default Color.ROYALBLUE
   */
  markerColor: {
    get: function () {
      return defaultMarkerColor;
    },
    set: function (value) {
      defaultMarkerColor = value;
    },
  },
  /**
   * Gets or sets the default color of polylines and polygon outlines.
   * @memberof GeoJsonDataSource
   * @type {Color}
   * @default Color.BLACK
   */
  stroke: {
    get: function () {
      return defaultStroke;
    },
    set: function (value) {
      defaultStroke = value;
    },
  },
  /**
   * Gets or sets the default width of polylines and polygon outlines.
   * @memberof GeoJsonDataSource
   * @type {Number}
   * @default 2.0
   */
  strokeWidth: {
    get: function () {
      return defaultStrokeWidth;
    },
    set: function (value) {
      defaultStrokeWidth = value;
    },
  },
  /**
   * Gets or sets default color for polygon interiors.
   * @memberof GeoJsonDataSource
   * @type {Color}
   * @default Color.YELLOW
   */
  fill: {
    get: function () {
      return defaultFill;
    },
    set: function (value) {
      defaultFill = value;
    },
  },
  /**
   * Gets or sets default of whether to clamp to the ground.
   * @memberof GeoJsonDataSource
   * @type {Boolean}
   * @default false
   */
  clampToGround: {
    get: function () {
      return defaultClampToGround;
    },
    set: function (value) {
      defaultClampToGround = value;
    },
  },

  /**
   * Gets an object that maps the name of a crs to a callback function which takes a GeoJSON coordinate
   * and transforms it into a WGS84 Earth-fixed Cartesian.  Older versions of GeoJSON which
   * supported the EPSG type can be added to this list as well, by specifying the complete EPSG name,
   * for example 'EPSG:4326'.
   * @memberof GeoJsonDataSource
   * @type {Object}
   */
  crsNames: {
    get: function () {
      return crsNames;
    },
  },

  /**
   * Gets an object that maps the href property of a crs link to a callback function
   * which takes the crs properties object and returns a Promise that resolves
   * to a function that takes a GeoJSON coordinate and transforms it into a WGS84 Earth-fixed Cartesian.
   * Items in this object take precedence over those defined in <code>crsLinkHrefs</code>, assuming
   * the link has a type specified.
   * @memberof GeoJsonDataSource
   * @type {Object}
   */
  crsLinkHrefs: {
    get: function () {
      return crsLinkHrefs;
    },
  },

  /**
   * Gets an object that maps the type property of a crs link to a callback function
   * which takes the crs properties object and returns a Promise that resolves
   * to a function that takes a GeoJSON coordinate and transforms it into a WGS84 Earth-fixed Cartesian.
   * Items in <code>crsLinkHrefs</code> take precedence over this object.
   * @memberof GeoJsonDataSource
   * @type {Object}
   */
  crsLinkTypes: {
    get: function () {
      return crsLinkTypes;
    },
  },
});

Object.defineProperties(GeoJsonDataSource.prototype, {
  /**
   * Gets or sets a human-readable name for this instance.
   * @memberof GeoJsonDataSource.prototype
   * @type {String}
   */
  name: {
    get: function () {
      return this._name;
    },
    set: function (value) {
      if (this._name !== value) {
        this._name = value;
        this._changed.raiseEvent(this);
      }
    },
  },
  /**
   * This DataSource only defines static data, therefore this property is always undefined.
   * @memberof GeoJsonDataSource.prototype
   * @type {DataSourceClock}
   */
  clock: {
    value: undefined,
    writable: false,
  },
  /**
   * Gets the collection of {@link Entity} instances.
   * @memberof GeoJsonDataSource.prototype
   * @type {EntityCollection}
   */
  entities: {
    get: function () {
      return this._entityCollection;
    },
  },
  /**
   * Gets a value indicating if the data source is currently loading data.
   * @memberof GeoJsonDataSource.prototype
   * @type {Boolean}
   */
  isLoading: {
    get: function () {
      return this._isLoading;
    },
  },
  /**
   * Gets an event that will be raised when the underlying data changes.
   * @memberof GeoJsonDataSource.prototype
   * @type {Event}
   */
  changedEvent: {
    get: function () {
      return this._changed;
    },
  },
  /**
   * Gets an event that will be raised if an error is encountered during processing.
   * @memberof GeoJsonDataSource.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 GeoJsonDataSource.prototype
   * @type {Event}
   */
  loadingEvent: {
    get: function () {
      return this._loading;
    },
  },
  /**
   * Gets whether or not this data source should be displayed.
   * @memberof GeoJsonDataSource.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 GeoJsonDataSource.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;
    },
  },
  /**
   * Gets the credit that will be displayed for the data source
   * @memberof GeoJsonDataSource.prototype
   * @type {Credit}
   */
  credit: {
    get: function () {
      return this._credit;
    },
  },
});

/**
 * Asynchronously loads the provided GeoJSON or TopoJSON data, replacing any existing data.
 *
 * @param {Resource|String|Object} data A url, GeoJSON object, or TopoJSON object to be loaded.
 * @param {Object} [options] An object with the following properties:
 * @param {String} [options.sourceUri] Overrides the url to use for resolving relative links.
 * @param {GeoJsonDataSource.describe} [options.describe=GeoJsonDataSource.defaultDescribeProperty] A function which returns a Property object (or just a string),
 *                                                                                which converts the properties into an html description.
 * @param {Number} [options.markerSize=GeoJsonDataSource.markerSize] The default size of the map pin created for each point, in pixels.
 * @param {String} [options.markerSymbol=GeoJsonDataSource.markerSymbol] The default symbol of the map pin created for each point.
 * @param {Color} [options.markerColor=GeoJsonDataSource.markerColor] The default color of the map pin created for each point.
 * @param {Color} [options.stroke=GeoJsonDataSource.stroke] The default color of polylines and polygon outlines.
 * @param {Number} [options.strokeWidth=GeoJsonDataSource.strokeWidth] The default width of polylines and polygon outlines.
 * @param {Color} [options.fill=GeoJsonDataSource.fill] The default color for polygon interiors.
 * @param {Boolean} [options.clampToGround=GeoJsonDataSource.clampToGround] true if we want the features clamped to the ground.
 * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas.
 *
 * @returns {Promise.<GeoJsonDataSource>} a promise that will resolve when the GeoJSON is loaded.
 */
GeoJsonDataSource.prototype.load = function (data, options) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(data)) {
    throw new DeveloperError("data is required.");
  }
  //>>includeEnd('debug');

  DataSource.setLoading(this, true);
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  // User specified credit
  var credit = options.credit;
  if (typeof credit === "string") {
    credit = new Credit(credit);
  }
  this._credit = credit;

  var promise = data;
  var sourceUri = options.sourceUri;
  if (typeof data === "string" || data instanceof Resource) {
    data = Resource.createIfNeeded(data);
    promise = data.fetchJson();
    sourceUri = defaultValue(sourceUri, data.getUrlComponent());

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

  options = {
    describe: defaultValue(options.describe, defaultDescribeProperty),
    markerSize: defaultValue(options.markerSize, defaultMarkerSize),
    markerSymbol: defaultValue(options.markerSymbol, defaultMarkerSymbol),
    markerColor: defaultValue(options.markerColor, defaultMarkerColor),
    strokeWidthProperty: new ConstantProperty(
      defaultValue(options.strokeWidth, defaultStrokeWidth)
    ),
    strokeMaterialProperty: new ColorMaterialProperty(
      defaultValue(options.stroke, defaultStroke)
    ),
    fillMaterialProperty: new ColorMaterialProperty(
      defaultValue(options.fill, defaultFill)
    ),
    clampToGround: defaultValue(options.clampToGround, defaultClampToGround),
  };

  var that = this;
  return when(promise, function (geoJson) {
    return load(that, geoJson, options, sourceUri);
  }).otherwise(function (error) {
    DataSource.setLoading(that, false);
    that._error.raiseEvent(that, error);
    console.log(error);
    return when.reject(error);
  });
};

/**
 * 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.
 */
GeoJsonDataSource.prototype.update = function (time) {
  return true;
};

function load(that, geoJson, options, sourceUri) {
  var name;
  if (defined(sourceUri)) {
    name = getFilenameFromUri(sourceUri);
  }

  if (defined(name) && that._name !== name) {
    that._name = name;
    that._changed.raiseEvent(that);
  }

  var typeHandler = geoJsonObjectTypes[geoJson.type];
  if (!defined(typeHandler)) {
    throw new RuntimeError("Unsupported GeoJSON object type: " + geoJson.type);
  }

  //Check for a Coordinate Reference System.
  var crs = geoJson.crs;
  var crsFunction = crs !== null ? defaultCrsFunction : null;

  if (defined(crs)) {
    if (!defined(crs.properties)) {
      throw new RuntimeError("crs.properties is undefined.");
    }

    var properties = crs.properties;
    if (crs.type === "name") {
      crsFunction = crsNames[properties.name];
      if (!defined(crsFunction)) {
        throw new RuntimeError("Unknown crs name: " + properties.name);
      }
    } else if (crs.type === "link") {
      var handler = crsLinkHrefs[properties.href];
      if (!defined(handler)) {
        handler = crsLinkTypes[properties.type];
      }

      if (!defined(handler)) {
        throw new RuntimeError(
          "Unable to resolve crs link: " + JSON.stringify(properties)
        );
      }

      crsFunction = handler(properties);
    } else if (crs.type === "EPSG") {
      crsFunction = crsNames["EPSG:" + properties.code];
      if (!defined(crsFunction)) {
        throw new RuntimeError("Unknown crs EPSG code: " + properties.code);
      }
    } else {
      throw new RuntimeError("Unknown crs type: " + crs.type);
    }
  }

  return when(crsFunction, function (crsFunction) {
    that._entityCollection.removeAll();

    // null is a valid value for the crs, but means the entire load process becomes a no-op
    // because we can't assume anything about the coordinates.
    if (crsFunction !== null) {
      typeHandler(that, geoJson, geoJson, crsFunction, options);
    }

    return when.all(that._promises, function () {
      that._promises.length = 0;
      DataSource.setLoading(that, false);
      return that;
    });
  });
}

/**
 * This callback is displayed as part of the GeoJsonDataSource class.
 * @callback GeoJsonDataSource.describe
 * @param {Object} properties The properties of the feature.
 * @param {String} nameProperty The property key that Cesium estimates to have the name of the feature.
 */
export default GeoJsonDataSource;
