import Layer from './Layer';
import extend from 'lodash.assign';
import reqwest from 'reqwest';
import GeoJSON from '../util/GeoJSON';
import Worker from '../util/Worker';
import Buffer from '../util/Buffer';
import Stringify from '../util/Stringify';
import PolygonLayer from './geometry/PolygonLayer';
import PolylineLayer from './geometry/PolylineLayer';
import PointLayer from './geometry/PointLayer';
import {latLon as LatLon} from '../geo/LatLon';
import {point as Point} from '../geo/Point';
import Geo from '../geo/Geo';
import PickingMaterial from '../engine/PickingMaterial';

// TODO: Allow filter method to be run inside a worker to improve performance
// TODO: Allow onEachFeature method to be run inside a worker to improve performance

class GeoJSONWorkerLayer extends Layer {
  constructor(geojson, options) {
    var defaults = {
      topojson: false,
      style: GeoJSON.defaultStyle,
      onEachFeature: null,
      onEachFeatureWorker: null,
      onAddAttributes: null,
      interactive: false,
      pointGeometry: null,
      onClick: null,
      headers: {}
    };

    var _options = extend({}, defaults, options);

    if (typeof options.style === 'object') {
      _options.style = extend({}, defaults.style, options.style);
    }

    super(_options);

    this._aborted = false;
    this._geojson = geojson;
  }

  _onAdd(world) {
    if (this._options.interactive) {
      // Worker layer always controls output to add a picking mesh
      this._pickingMesh = new THREE.Object3D();
    }

    // Process GeoJSON
    return this._process(this._geojson).catch((err) => {
      console.error(err);
    });
  }

  // Use workers to request and process GeoJSON, returning data structure
  // containing geometry and any supplementary data for output
  _process(_geojson) {
    return new Promise((resolve, reject) => {
      var style = this._options.style;

      // TODO: Convert to buffer and use transferrable objects
      if (typeof this._options.style === 'function') {
        style = Stringify.functionToString(this._options.style);
      }

      var pointGeometry = this._options.pointGeometry;

      // TODO: Convert to buffer and use transferrable objects
      if (typeof this._options.pointGeometry === 'function') {
        pointGeometry = Stringify.functionToString(this._options.pointGeometry);
      }

      var geojson = _geojson;
      var transferrables = [];

      if (typeof geojson !== 'string') {
        this._geojson = geojson = Buffer.stringToUint8Array(JSON.stringify(geojson));
        transferrables.push(geojson.buffer);
        this._execWorker(geojson, this._options.topojson, this._world._originPoint, style, this._options.interactive, pointGeometry, transferrables).then(() => {
          resolve();
        }).catch(reject);
      } else if (typeof this._options.filter === 'function' || typeof this._options.onEachFeature === 'function') {
        GeoJSONWorkerLayer.RequestGeoJSON(geojson).then((res) => {
          // if (this._aborted) {
          //   resolve();
          //   return;
          // }

          var fc = GeoJSON.collectFeatures(res, this._options.topojson);
          var features = fc.features;

          // Run filter, if provided
          if (this._options.filter) {
            fc.features = features.filter(this._options.filter);
          }

          if (this._options.onEachFeature) {
            var feature;
            for (var i = 0; i < features.length; i++) {
              feature = features[i];
              this._options.onEachFeature(feature);
            };
          }

          this._geojson = geojson = Buffer.stringToUint8Array(JSON.stringify(fc));
          transferrables.push(geojson.buffer);

          this._execWorker(geojson, false, this._options.headers, this._world._originPoint, style, this._options.interactive, pointGeometry, transferrables).then(() => {
            resolve();
          }).catch(reject);
        });
      } else {
        this._execWorker(geojson, this._options.topojson, this._options.headers, this._world._originPoint, style, this._options.interactive, pointGeometry, transferrables).then(() => {
          resolve();
        }).catch(reject);
      }
    });
  }

  _execWorker(geojson, topojson, headers, originPoint, style, interactive, pointGeometry, transferrables) {
    return new Promise((resolve, reject) => {
      // console.time('Worker round trip');

      Worker.exec('GeoJSONWorkerLayer.Process', [geojson, topojson, headers, originPoint, style, interactive, pointGeometry], transferrables).then((results) => {
        // console.timeEnd('Worker round trip');

        // if (this._aborted) {
        //   resolve();
        //   return;
        // }

        var processPromises = [];

        if (results.polygons) {
          processPromises.push(this._processPolygonResults(results.polygons));
        }

        if (results.polylines) {
          processPromises.push(this._processPolylineResults(results.polylines));
        }

        if (results.points) {
          processPromises.push(this._processPointResults(results.points));
        }

        if (processPromises.length > 0) {
          Promise.all(processPromises).then(() => {
            resolve();
          }).catch(reject);
        } else {
          resolve();
        }
      }).catch(reject);
    });
  }

  // TODO: Dedupe with polyline method
  _processPolygonResults(results) {
    return new Promise((resolve, reject) => {
      var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
      var splitNormals = Buffer.splitFloat32Array(results.attributes.normals);
      var splitColors = Buffer.splitFloat32Array(results.attributes.colors);
      var splitTops = Buffer.splitFloat32Array(results.attributes.tops);

      var splitOutlinePositions;
      var splitOutlineColors;

      if (results.outlineAttributes) {
        splitOutlinePositions = Buffer.splitFloat32Array(results.outlineAttributes.positions);
        splitOutlineColors = Buffer.splitFloat32Array(results.outlineAttributes.colors);
      }

      var splitProperties;
      if (results.properties) {
        splitProperties = Buffer.splitUint8Array(results.properties);
      }

      var flats = results.flats;

      var objects = [];
      var outlineObjects = [];

      var obj;
      var pickingId;
      var pickingIds;
      var properties;

      var polygonAttributeLengths = {
        positions: 3,
        normals: 3,
        colors: 3,
        tops: 1
      };

      var polygonOutlineAttributeLengths = {
        positions: 3,
        colors: 3
      };

      for (var i = 0; i < splitPositions.length; i++) {
        if (splitProperties && splitProperties[i]) {
          properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
        } else {
          properties = {};
        }

        // WORKERS: obj.attributes should actually an array of polygons for
        // the feature, though the current logic isn't aware of that
        obj = {
          attributes: [{
            positions: splitPositions[i],
            normals: splitNormals[i],
            colors: splitColors[i],
            tops: splitTops[i]
          }],
          properties: properties,
          flat: flats[i]
        };

        // WORKERS: If interactive, generate unique ID for each feature, create
        // the buffer attributes and set up event listeners
        if (this._options.interactive) {
          pickingId = this.getPickingId();

          pickingIds = new Float32Array(splitPositions[i].length / 3);
          Buffer.fillTypedArray(pickingIds, pickingId);

          obj.attributes[0].pickingIds = pickingIds;

          polygonAttributeLengths.pickingIds = 1;

          this._addPicking(pickingId, properties);
        }

        // TODO: Make this specific to polygon attributes
        if (typeof this._options.onAddAttributes === 'function') {
          var customAttributes = this._options.onAddAttributes(obj.attributes[0], properties);
          var customAttribute;
          for (var key in customAttributes) {
            customAttribute = customAttributes[key];
            obj.attributes[0][key] = customAttribute.value;
            polygonAttributeLengths[key] = customAttribute.length;
          }
        }

        objects.push(obj);
      }

      for (var i = 0; i < splitOutlinePositions.length; i++) {
        obj = {
          attributes: [{
            positions: splitOutlinePositions[i],
            colors: splitOutlineColors[i]
          }],
          flat: true
        };

        outlineObjects.push(obj);
      }

      var polygonAttributes = [];
      var polygonOutlineAttributes = [];

      var polygonFlat = true;

      for (var i = 0; i < objects.length; i++) {
        obj = objects[i];

        // TODO: Work out why obj.flat is rarely set to something other than
        // true or false. Potentially undefined.
        if (polygonFlat && obj.flat === false) {
          polygonFlat = false;
        }

        var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
        polygonAttributes.push(bufferAttributes);
      };

      for (var i = 0; i < outlineObjects.length; i++) {
        obj = outlineObjects[i];

        var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
        polygonOutlineAttributes.push(bufferAttributes);
      };

      var outputPromises = [];

      var style;

      if (polygonAttributes.length > 0) {
        var mergedPolygonAttributes = Buffer.mergeAttributes(polygonAttributes);

        // TODO: Make this work when style is a function per feature
        style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
        style = extend({}, GeoJSON.defaultStyle, style);

        outputPromises.push(this._setPolygonMesh(mergedPolygonAttributes, polygonAttributeLengths, style, polygonFlat));
      }

      if (polygonOutlineAttributes.length > 0) {
        var mergedPolygonOutlineAttributes = Buffer.mergeAttributes(polygonOutlineAttributes);

        style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
        style = extend({}, GeoJSON.defaultStyle, style);

        if (style.outlineRenderOrder !== undefined) {
          style.lineRenderOrder = style.outlineRenderOrder;
        } else {
          style.lineRenderOrder = (style.renderOrder) ? style.renderOrder + 1 : 4;
        }

        if (style.outlineWidth) {
          style.lineWidth = style.outlineWidth;
        }

        outputPromises.push(this._setPolylineMesh(mergedPolygonOutlineAttributes, polygonOutlineAttributeLengths, style, true));
      }

      Promise.all(outputPromises).then((results) => {
        var [polygonResult, outlineResult] = results;

        if (polygonResult) {
          this._polygonMesh = polygonResult.mesh;
          this.add(this._polygonMesh);

          if (polygonResult.pickingMesh) {
            this._pickingMesh.add(polygonResult.pickingMesh);
          }
        }

        if (outlineResult) {
          this.add(outlineResult.mesh);
        }

        resolve();
      }).catch(reject);
    });
  }

  // TODO: Dedupe with polygon method
  _processPolylineResults(results) {
    return new Promise((resolve, reject) => {
      var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
      var splitColors = Buffer.splitFloat32Array(results.attributes.colors);

      var splitProperties;
      if (results.properties) {
        splitProperties = Buffer.splitUint8Array(results.properties);
      }

      var flats = results.flats;

      var objects = [];
      var obj;
      var pickingId;
      var pickingIds;
      var properties;

      var polylineAttributeLengths = {
        positions: 3,
        colors: 3
      };

      for (var i = 0; i < splitPositions.length; i++) {
        if (splitProperties && splitProperties[i]) {
          properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
        } else {
          properties = {};
        }

        // WORKERS: obj.attributes should actually an array of polygons for
        // the feature, though the current logic isn't aware of that
        obj = {
          attributes: [{
            positions: splitPositions[i],
            colors: splitColors[i]
          }],
          properties: properties,
          flat: flats[i]
        };

        // WORKERS: If interactive, generate unique ID for each feature, create
        // the buffer attributes and set up event listeners
        if (this._options.interactive) {
          pickingId = this.getPickingId();

          pickingIds = new Float32Array(splitPositions[i].length / 3);
          Buffer.fillTypedArray(pickingIds, pickingId);

          obj.attributes[0].pickingIds = pickingIds;

          polylineAttributeLengths.pickingIds = 1;

          this._addPicking(pickingId, properties);
        }

        // TODO: Make this specific to polyline attributes
        if (typeof this._options.onAddAttributes === 'function') {
          var customAttributes = this._options.onAddAttributes(obj.attributes[0], properties);
          var customAttribute;
          for (var key in customAttributes) {
            customAttribute = customAttributes[key];
            obj.attributes[0][key] = customAttribute.value;
            polylineAttributeLengths[key] = customAttribute.length;
          }
        }

        objects.push(obj);
      }

      var polylineAttributes = [];

      var polylineFlat = true;

      for (var i = 0; i < objects.length; i++) {
        obj = objects[i];

        if (polylineFlat && !obj.flat) {
          polylineFlat = false;
        }

        var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
        polylineAttributes.push(bufferAttributes);
      };

      if (polylineAttributes.length > 0) {
        var mergedPolylineAttributes = Buffer.mergeAttributes(polylineAttributes);

        // TODO: Make this work when style is a function per feature
        var style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
        style = extend({}, GeoJSON.defaultStyle, style);

        this._setPolylineMesh(mergedPolylineAttributes, polylineAttributeLengths, style, polylineFlat).then((result) => {
          this._polylineMesh = result.mesh;
          this.add(this._polylineMesh);

          if (result.pickingMesh) {
            this._pickingMesh.add(result.pickingMesh);
          }

          resolve();
        }).catch(reject);
      } else {
        resolve();
      }
    });
  }

  _processPointResults(results) {
    return new Promise((resolve, reject) => {
      var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
      var splitNormals = Buffer.splitFloat32Array(results.attributes.normals);
      var splitColors = Buffer.splitFloat32Array(results.attributes.colors);

      var splitProperties;
      if (results.properties) {
        splitProperties = Buffer.splitUint8Array(results.properties);
      }

      var flats = results.flats;

      var objects = [];
      var obj;
      var pickingId;
      var pickingIds;
      var properties;

      var pointAttributeLengths = {
        positions: 3,
        normals: 3,
        colors: 3
      };

      for (var i = 0; i < splitPositions.length; i++) {
        if (splitProperties && splitProperties[i]) {
          properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
        } else {
          properties = {};
        }

        // WORKERS: obj.attributes should actually an array of polygons for
        // the feature, though the current logic isn't aware of that
        obj = {
          attributes: [{
            positions: splitPositions[i],
            normals: splitNormals[i],
            colors: splitColors[i]
          }],
          properties: properties,
          flat: flats[i]
        };

        // WORKERS: If interactive, generate unique ID for each feature, create
        // the buffer attributes and set up event listeners
        if (this._options.interactive) {
          pickingId = this.getPickingId();

          pickingIds = new Float32Array(splitPositions[i].length / 3);
          Buffer.fillTypedArray(pickingIds, pickingId);

          obj.attributes[0].pickingIds = pickingIds;

          pointAttributeLengths.pickingIds = 1;

          this._addPicking(pickingId, properties);
        }

        // TODO: Make this specific to polygon attributes
        if (typeof this._options.onAddAttributes === 'function') {
          var customAttributes = this._options.onAddAttributes(obj.attributes[0], properties);
          var customAttribute;
          for (var key in customAttributes) {
            customAttribute = customAttributes[key];
            obj.attributes[0][key] = customAttribute.value;
            pointAttributeLengths[key] = customAttribute.length;
          }
        }

        objects.push(obj);
      }

      var pointAttributes = [];

      var pointFlat = true;

      for (var i = 0; i < objects.length; i++) {
        obj = objects[i];

        if (pointFlat && !obj.flat) {
          pointFlat = false;
        }

        var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
        pointAttributes.push(bufferAttributes);
      };

      if (pointAttributes.length > 0) {
        var mergedPointAttributes = Buffer.mergeAttributes(pointAttributes);

        // TODO: Make this work when style is a function per feature
        var style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
        style = extend({}, GeoJSON.defaultStyle, style);

        this._setPointMesh(mergedPointAttributes, pointAttributeLengths, style, pointFlat).then((result) => {
          this._pointMesh = result.mesh;
          this.add(this._pointMesh);

          if (result.pickingMesh) {
            this._pickingMesh.add(result.pickingMesh);
          }

          resolve();
        }).catch(reject);
      } else {
        resolve();
      }
    });
  }

  // TODO: At some point this needs to return all the features to the main thread
  // so it can generate meshes and output to the scene, as well as perhaps creating
  // individual layers / components for each feature to track things like picking
  // and properties
  //
  // TODO: Find a way so the origin point isn't needed to be passed in as it
  // feels a bit messy and against the idea of a static Geo class
  //
  // TODO: Support passing custom geometry for point layers
  static Process(geojson, topojson, headers, originPoint, _style, _properties, _pointGeometry) {
    return new Promise((resolve, reject) => {
      GeoJSONWorkerLayer.ProcessGeoJSON(geojson, headers).then((res) => {
        // Collects features into a single FeatureCollection
        //
        // Also converts TopoJSON to GeoJSON if instructed
        var geojson = GeoJSON.collectFeatures(res, topojson);

        // TODO: Check that GeoJSON is valid / usable

        var features = geojson.features;

        // TODO: Run filter, if provided (must be static)

        var pointScale;
        var polygons = [];
        var polylines = [];
        var points = [];

        // Deserialise style function if provided
        if (typeof _style === 'string') {
          _style = Stringify.stringToFunction(_style);
        }

        // Assume that a style won't be set per feature
        var style = _style;

        var pointGeometry;
        // Deserialise pointGeometry function if provided
        if (typeof _pointGeometry === 'string') {
          pointGeometry = Stringify.stringToFunction(_pointGeometry);
        }

        var feature;
        for (var i = 0; i < features.length; i++) {
          feature = features[i];

          var geometry = feature.geometry;
          var coordinates = (geometry.coordinates) ? geometry.coordinates : null;

          if (!coordinates || !geometry) {
            return;
          }

          // Get per-feature style object, if provided
          if (typeof _style === 'function') {
            style = extend({}, GeoJSON.defaultStyle, _style(feature));
            // console.log(feature, style);
          }

          if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
            coordinates = (PolygonLayer.isSingle(coordinates)) ? [coordinates] : coordinates;

            var converted = coordinates.map(_coordinates => {
              return _coordinates.map(ring => {
                return ring.map(coordinate => {
                  return LatLon(coordinate[1], coordinate[0]);
                });
              });
            });

            var point;
            var projected = converted.map((_coordinates) => {
              return _coordinates.map((ring) => {
                return ring.map((latlon) => {
                  point = Geo.latLonToPoint(latlon)._subtract(originPoint);

                  if (!pointScale) {
                    pointScale = Geo.pointScale(latlon);
                  }

                  return point;
                });
              });
            });

            var polygon = {
              projected: projected,
              options: {
                pointScale: pointScale,
                style: style
              }
            };

            if (_properties) {
              polygon.properties = feature.properties;
            }

            polygons.push(polygon);
          }

          if (geometry.type === 'LineString' || geometry.type === 'MultiLineString') {
            coordinates = (PolylineLayer.isSingle(coordinates)) ? [coordinates] : coordinates;

            var converted = coordinates.map(_coordinates => {
              return _coordinates.map(coordinate => {
                return LatLon(coordinate[1], coordinate[0]);
              });
            });

            var point;
            var projected = converted.map((_coordinates) => {
              return _coordinates.map((latlon) => {
                point = Geo.latLonToPoint(latlon)._subtract(originPoint);

                if (!pointScale) {
                  pointScale = Geo.pointScale(latlon);
                }

                return point;
              });
            });

            var polyline = {
              projected: projected,
              options: {
                pointScale: pointScale,
                style: style
              }
            };

            if (_properties) {
              polyline.properties = feature.properties;
            }

            polylines.push(polyline);
          }

          if (geometry.type === 'Point' || geometry.type === 'MultiPoint') {
            coordinates = (PointLayer.isSingle(coordinates)) ? [coordinates] : coordinates;

            var converted = coordinates.map(coordinate => {
              return LatLon(coordinate[1], coordinate[0]);
            });

            var point;
            var projected = converted.map((latlon) => {
              point = Geo.latLonToPoint(latlon)._subtract(originPoint);

              if (!pointScale) {
                pointScale = Geo.pointScale(latlon);
              }

              return point;
            });

            var point = {
              projected: projected,
              options: {
                pointGeometry: pointGeometry(feature),
                pointScale: pointScale,
                style: style
              }
            };

            if (_properties) {
              point.properties = feature.properties;
            }

            points.push(point);
          }
        };

        var polygonBufferPromises = [];
        var polylineBufferPromises = [];
        var pointBufferPromises = [];

        var polygon;
        for (var i = 0; i < polygons.length; i++) {
          polygon = polygons[i];
          polygonBufferPromises.push(PolygonLayer.SetBufferAttributes(polygon.projected, polygon.options));
        };

        var polyline;
        for (var i = 0; i < polylines.length; i++) {
          polyline = polylines[i];
          polylineBufferPromises.push(PolylineLayer.SetBufferAttributes(polyline.projected, polyline.options));
        };

        var point;
        for (var i = 0; i < points.length; i++) {
          point = points[i];
          pointBufferPromises.push(PointLayer.SetBufferAttributes(point.projected, point.options));
        };

        var data = {};
        var transferrables = [];

        // TODO: Make this work with polylines too
        // TODO: Make this so it's not a nest of promises
        GeoJSONWorkerLayer.ProcessPolygons(polygonBufferPromises, polygons, _properties).then((result) => {
          data.polygons = result.data;
          transferrables = transferrables.concat(result.transferrables);

          GeoJSONWorkerLayer.ProcessPolylines(polylineBufferPromises, polylines, _properties).then((result) => {
            data.polylines = result.data;
            transferrables = transferrables.concat(result.transferrables);

            GeoJSONWorkerLayer.ProcessPoints(pointBufferPromises, points, _properties).then((result) => {
              data.points = result.data;
              transferrables = transferrables.concat(result.transferrables);

              resolve({
                data: data,
                transferrables: transferrables
              });
            });
          });
        });
      }).catch(reject);
    });
  }

  static ProcessPolygons(polygonPromises, polygons, _properties) {
    return new Promise((resolve, reject) => {
      Promise.all(polygonPromises).then((results) => {
        var transferrables = [];

        var positions = [];
        var normals = [];
        var colors = [];
        var tops = [];

        var outlinePositions = [];
        var outlineColors = [];

        var properties = [];

        var flats = [];
        var polygon;

        var result;
        for (var i = 0; i < results.length; i++) {
          result = results[i];

          polygon = polygons[i];

          // WORKERS: Making this a typed array will speed up transfer time
          // As things stand this adds on a few milliseconds
          flats.push(result.flat);

          // WORKERS: result.attributes is actually an array of polygons for each
          // feature, though the current logic isn't keeping these all together

          var attributes;
          for (var j = 0; j < result.attributes.length; j++) {
            attributes = result.attributes[j];

            positions.push(attributes.positions);
            normals.push(attributes.normals);
            colors.push(attributes.colors);
            tops.push(attributes.tops);

            if (_properties) {
              properties.push(Buffer.stringToUint8Array(JSON.stringify(polygon.properties)));
            }
          };

          var outlineAttributes;
          for (var j = 0; j < result.outlineAttributes.length; j++) {
            outlineAttributes = result.outlineAttributes[j];

            outlinePositions.push(outlineAttributes.positions);
            outlineColors.push(outlineAttributes.colors);
          };
        };

        var mergedAttributes = {
          positions: Buffer.mergeFloat32Arrays(positions),
          normals: Buffer.mergeFloat32Arrays(normals),
          colors: Buffer.mergeFloat32Arrays(colors),
          tops: Buffer.mergeFloat32Arrays(tops)
        };

        var mergedOutlineAttributes = {
          positions: Buffer.mergeFloat32Arrays(outlinePositions),
          colors: Buffer.mergeFloat32Arrays(outlineColors)
        };

        transferrables.push(mergedAttributes.positions[0].buffer);
        transferrables.push(mergedAttributes.positions[1].buffer);

        transferrables.push(mergedAttributes.normals[0].buffer);
        transferrables.push(mergedAttributes.normals[1].buffer);

        transferrables.push(mergedAttributes.colors[0].buffer);
        transferrables.push(mergedAttributes.colors[1].buffer);

        transferrables.push(mergedAttributes.tops[0].buffer);
        transferrables.push(mergedAttributes.tops[1].buffer);

        transferrables.push(mergedOutlineAttributes.positions[0].buffer);
        transferrables.push(mergedOutlineAttributes.positions[1].buffer);

        transferrables.push(mergedOutlineAttributes.colors[0].buffer);
        transferrables.push(mergedOutlineAttributes.colors[1].buffer);

        var mergedProperties;
        if (_properties) {
          mergedProperties = Buffer.mergeUint8Arrays(properties);

          transferrables.push(mergedProperties[0].buffer);
          transferrables.push(mergedProperties[1].buffer);
        }

        var output = {
          attributes: mergedAttributes,
          outlineAttributes: mergedOutlineAttributes,
          flats: flats
        };

        if (_properties) {
          output.properties = mergedProperties;
        }

        // TODO: Also return GeoJSON features that can be mapped to objects on
        // the main thread. Allow user to provide filter / toggles to only return
        // properties from the GeoJSON that they need (eg. don't return geometry,
        // or don't return properties.height)
        resolve({
          data: output,
          transferrables: transferrables
        });
      }).catch(reject);
    });
  }

  static ProcessPolylines(polylinePromises, polylines, _properties) {
    return new Promise((resolve, reject) => {
      Promise.all(polylinePromises).then((results) => {
        var transferrables = [];

        var positions = [];
        var colors = [];

        var properties = [];

        var flats = [];
        var polyline;

        var result;
        for (var i = 0; i < results.length; i++) {
          result = results[i];

          polyline = polylines[i];

          // WORKERS: Making this a typed array will speed up transfer time
          // As things stand this adds on a few milliseconds
          flats.push(result.flat);

          // WORKERS: result.attributes is actually an array of polygons for each
          // feature, though the current logic isn't keeping these all together

          var attributes;
          for (var j = 0; j < result.attributes.length; j++) {
            attributes = result.attributes[j];

            positions.push(attributes.positions);
            colors.push(attributes.colors);

            if (_properties) {
              properties.push(Buffer.stringToUint8Array(JSON.stringify(polyline.properties)));
            }
          };
        };

        var mergedAttributes = {
          positions: Buffer.mergeFloat32Arrays(positions),
          colors: Buffer.mergeFloat32Arrays(colors)
        };

        transferrables.push(mergedAttributes.positions[0].buffer);
        transferrables.push(mergedAttributes.positions[1].buffer);

        transferrables.push(mergedAttributes.colors[0].buffer);
        transferrables.push(mergedAttributes.colors[1].buffer);

        var mergedProperties;
        if (_properties) {
          mergedProperties = Buffer.mergeUint8Arrays(properties);

          transferrables.push(mergedProperties[0].buffer);
          transferrables.push(mergedProperties[1].buffer);
        }

        var output = {
          attributes: mergedAttributes,
          flats: flats
        };

        if (_properties) {
          output.properties = mergedProperties;
        }

        // TODO: Also return GeoJSON features that can be mapped to objects on
        // the main thread. Allow user to provide filter / toggles to only return
        // properties from the GeoJSON that they need (eg. don't return geometry,
        // or don't return properties.height)
        resolve({
          data: output,
          transferrables: transferrables
        });
      }).catch(reject);
    });
  }

  // TODO: Dedupe with ProcessPolygons as they are identical
  static ProcessPoints(pointPromises, points, _properties) {
    return new Promise((resolve, reject) => {
      Promise.all(pointPromises).then((results) => {
        var transferrables = [];

        var positions = [];
        var normals = [];
        var colors = [];

        var properties = [];

        var flats = [];
        var point;

        var result;
        for (var i = 0; i < results.length; i++) {
          result = results[i];

          point = points[i];

          // WORKERS: Making this a typed array will speed up transfer time
          // As things stand this adds on a few milliseconds
          flats.push(result.flat);

          // WORKERS: result.attributes is actually an array of polygons for each
          // feature, though the current logic isn't keeping these all together

          var attributes;
          for (var j = 0; j < result.attributes.length; j++) {
            attributes = result.attributes[j];

            positions.push(attributes.positions);
            normals.push(attributes.normals);
            colors.push(attributes.colors);

            if (_properties) {
              properties.push(Buffer.stringToUint8Array(JSON.stringify(polygon.properties)));
            }
          };
        };

        var mergedAttributes = {
          positions: Buffer.mergeFloat32Arrays(positions),
          normals: Buffer.mergeFloat32Arrays(normals),
          colors: Buffer.mergeFloat32Arrays(colors)
        };

        transferrables.push(mergedAttributes.positions[0].buffer);
        transferrables.push(mergedAttributes.positions[1].buffer);

        transferrables.push(mergedAttributes.normals[0].buffer);
        transferrables.push(mergedAttributes.normals[1].buffer);

        transferrables.push(mergedAttributes.colors[0].buffer);
        transferrables.push(mergedAttributes.colors[1].buffer);

        var mergedProperties;
        if (_properties) {
          mergedProperties = Buffer.mergeUint8Arrays(properties);

          transferrables.push(mergedProperties[0].buffer);
          transferrables.push(mergedProperties[1].buffer);
        }

        var output = {
          attributes: mergedAttributes,
          flats: flats
        };

        if (_properties) {
          output.properties = mergedProperties;
        }

        // TODO: Also return GeoJSON features that can be mapped to objects on
        // the main thread. Allow user to provide filter / toggles to only return
        // properties from the GeoJSON that they need (eg. don't return geometry,
        // or don't return properties.height)
        resolve({
          data: output,
          transferrables: transferrables
        });
      }).catch(reject);
    });
  }

  static ProcessGeoJSON(geojson, headers) {
    if (typeof geojson === 'string') {
      return GeoJSONWorkerLayer.RequestGeoJSON(geojson, headers);
    } else {
      return Promise.resolve(JSON.parse(Buffer.uint8ArrayToString(geojson)));
    }
  }

  static RequestGeoJSON(path, headers) {
    return reqwest({
      url: path,
      type: 'json',
      crossOrigin: true,
      headers: headers
    });
  }

  // Create and store mesh from buffer attributes
  //
  // Could make this an abstract method for each geometry layer
  _setPolygonMesh(attributes, attributeLengths, style, flat) {
    if (!this._world) {
      return Promise.reject();
    }

    return PolygonLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox);
  }

  _setPolylineMesh(attributes, attributeLengths, style, flat) {
    if (!this._world) {
      return Promise.reject();
    }

    return PolylineLayer.SetMesh(attributes, attributeLengths, flat, style, this._options);
  }

  _setPointMesh(attributes, attributeLengths, style, flat) {
    if (!this._world) {
      return Promise.reject();
    }

    return PointLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox);
  }

  // Set up and re-emit interaction events
  _addPicking(pickingId, properties) {
    this._world.on('pick-click-' + pickingId, (pickingId, point2d, point3d, intersects) => {
      this._world.emit('click', this, properties, point2d, point3d);
    });

    this._world.on('pick-hover-' + pickingId, (pickingId, point2d, point3d, intersects) => {
      this._world.emit('hover', this, properties, point2d, point3d);
    });
  }

  // TODO: Finish cleanup
  destroy() {
    // Run common destruction logic from parent
    super.destroy();
  }
}

export default GeoJSONWorkerLayer;

var noNew = function(geojson, options) {
  return new GeoJSONWorkerLayer(geojson, options);
};

export {noNew as geoJSONWorkerLayer};
