import Cartesian3 from "../../Core/Cartesian3.js";
import Check from "../../Core/Check.js";
import ComponentDatatype from "../../Core/ComponentDatatype.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import Ellipsoid from "../../Core/Ellipsoid.js";
import IndexDatatype from "../../Core/IndexDatatype.js";
import Matrix4 from "../../Core/Matrix4.js";
import PrimitiveType from "../../Core/PrimitiveType.js";
import RuntimeError from "../../Core/RuntimeError.js";
import Transforms from "../../Core/Transforms.js";
import AttributeType from "../AttributeType.js";
import JsonMetadataTable from "../JsonMetadataTable.js";
import MetadataSchema from "../MetadataSchema.js";
import ModelComponents from "../ModelComponents.js";
import PropertyTable from "../PropertyTable.js";
import ResourceLoader from "../ResourceLoader.js";
import StructuralMetadata from "../StructuralMetadata.js";
import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
import Buffer from "../../Renderer/Buffer.js";
import BufferUsage from "../../Renderer/BufferUsage.js";

/**
 * Loads a GeoJson model as part of the <code>MAXAR_content_geojson</code> extension with the following constraints:
 * <ul>
 *   <li>The top level GeoJSON type must be FeatureCollection or Feature.</li>
 *   <li>The geometry types must be LineString, MultiLineString, MultiPolygon, or Polygon.</li>
 *   <li>All geometries are converted to geodesic lines.</li>
 *   <li>Only WGS84 geographic coordinates are supported.</li>
 * </ul>
 * <p>
 * Implements the {@link ResourceLoader} interface.
 * </p>
 *
 * @alias GeoJsonLoader
 * @constructor
 * @augments ResourceLoader
 * @private
 *
 * @param {Object} options Object with the following properties:
 * @param {Object} options.geoJson The GeoJson object.
 */
export default function GeoJsonLoader(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("options.geoJson", options.geoJson);
  //>>includeEnd('debug');

  this._geoJson = options.geoJson;
  this._promise = undefined;
  this._process = function (loader, frameState) {};
  this._components = undefined;
}

if (defined(Object.create)) {
  GeoJsonLoader.prototype = Object.create(ResourceLoader.prototype);
  GeoJsonLoader.prototype.constructor = GeoJsonLoader;
}

Object.defineProperties(GeoJsonLoader.prototype, {
  /**
   * A promise that resolves to the resource when the resource is ready, or undefined if the resource has not yet started loading.
   *
   * @memberof GeoJsonLoader.prototype
   *
   * @type {Promise.<GeoJsonLoader>|Undefined}
   * @readonly
   * @private
   */
  promise: {
    get: function () {
      return this._promise;
    },
  },
  /**
   * The cache key of the resource.
   *
   * @memberof GeoJsonLoader.prototype
   *
   * @type {String}
   * @readonly
   * @private
   */
  cacheKey: {
    get: function () {
      return undefined;
    },
  },
  /**
   * The loaded components.
   *
   * @memberof GeoJsonLoader.prototype
   *
   * @type {ModelComponents.Components}
   * @readonly
   * @private
   */
  components: {
    get: function () {
      return this._components;
    },
  },
});

/**
 * Loads the resource.
 * @returns {Promise.<GeoJsonLoader>} A promise which resolves to the loader when the resource loading is completed.
 * @private
 */
GeoJsonLoader.prototype.load = function () {
  const loader = this;
  const promise = new Promise(function (resolve) {
    loader._process = function (loader, frameState) {
      if (defined(loader._components)) {
        return;
      }

      loader._components = parse(loader._geoJson, frameState);
      loader._geoJson = undefined;
      resolve(loader);
    };
  });

  this._promise = promise;
  return promise;
};

/**
 * Processes the resource until it becomes ready.
 *
 * @param {FrameState} frameState The frame state.
 * @private
 */
GeoJsonLoader.prototype.process = function (frameState) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("frameState", frameState);
  //>>includeEnd('debug');

  this._process(this, frameState);
};

function ParsedFeature() {
  this.lines = undefined;
  this.properties = undefined;
}

function ParseResult() {
  this.features = [];
}

function parsePosition(position) {
  const x = position[0];
  const y = position[1];
  const z = defaultValue(position[2], 0.0);
  return new Cartesian3(x, y, z);
}

function parseLineString(coordinates) {
  const positionsLength = coordinates.length;
  const line = new Array(positionsLength);
  for (let i = 0; i < positionsLength; i++) {
    line[i] = parsePosition(coordinates[i]);
  }
  const lines = [line];
  return lines;
}

function parseMultiLineString(coordinates) {
  const linesLength = coordinates.length;
  const lines = new Array(linesLength);
  for (let i = 0; i < linesLength; i++) {
    lines[i] = parseLineString(coordinates[i])[0];
  }
  return lines;
}

function parsePolygon(coordinates) {
  // Treat exterior polygon and interior polygons as lines
  const linesLength = coordinates.length;
  const lines = new Array(linesLength);
  for (let i = 0; i < linesLength; i++) {
    lines[i] = parseLineString(coordinates[i])[0];
  }
  return lines;
}

function parseMultiPolygon(coordinates) {
  const polygonsLength = coordinates.length;
  const lines = [];
  for (let i = 0; i < polygonsLength; i++) {
    Array.prototype.push.apply(lines, parsePolygon(coordinates[i]));
  }
  return lines;
}

const geometryTypes = {
  LineString: parseLineString,
  MultiLineString: parseMultiLineString,
  MultiPolygon: parseMultiPolygon,
  Polygon: parsePolygon,
};

function parseFeature(feature, result) {
  if (!defined(feature.geometry)) {
    return;
  }

  const geometryType = feature.geometry.type;
  const geometryFunction = geometryTypes[geometryType];
  const coordinates = feature.geometry.coordinates;

  if (!defined(geometryFunction)) {
    return;
  }

  if (!defined(coordinates)) {
    return;
  }

  const parsedFeature = new ParsedFeature();
  parsedFeature.lines = geometryFunction(coordinates);
  parsedFeature.properties = feature.properties;

  result.features.push(parsedFeature);
}

function parseFeatureCollection(featureCollection, result) {
  const features = featureCollection.features;
  const featuresLength = features.length;
  for (let i = 0; i < featuresLength; i++) {
    parseFeature(features[i], result);
  }
}

const geoJsonObjectTypes = {
  FeatureCollection: parseFeatureCollection,
  Feature: parseFeature,
};

const scratchCartesian = new Cartesian3();

function parse(geoJson, frameState) {
  const result = new ParseResult();

  // Parse the GeoJSON
  const parseFunction = geoJsonObjectTypes[geoJson.type];
  if (defined(parseFunction)) {
    parseFunction(geoJson, result);
  }

  const featureCount = result.features.length;

  if (featureCount === 0) {
    throw new RuntimeError("GeoJSON must have at least one feature");
  }

  // Allocate space for property values
  const properties = {};
  for (let i = 0; i < featureCount; i++) {
    const feature = result.features[i];
    const featureProperties = defaultValue(
      feature.properties,
      defaultValue.EMPTY_OBJECT
    );
    for (const propertyId in featureProperties) {
      if (featureProperties.hasOwnProperty(propertyId)) {
        if (!defined(properties[propertyId])) {
          properties[propertyId] = new Array(featureCount);
        }
      }
    }
  }

  // Fill in the property values. Default to empty string for undefined values.
  for (let i = 0; i < featureCount; i++) {
    const feature = result.features[i];
    for (const propertyId in properties) {
      if (properties.hasOwnProperty(propertyId)) {
        const value = defaultValue(feature.properties[propertyId], "");
        properties[propertyId][i] = value;
      }
    }
  }

  const jsonMetadataTable = new JsonMetadataTable({
    count: featureCount,
    properties: properties,
  });

  const propertyTable = new PropertyTable({
    id: 0,
    count: featureCount,
    jsonMetadataTable: jsonMetadataTable,
  });
  const propertyTables = [propertyTable];

  const schema = new MetadataSchema({});

  const structuralMetadata = new StructuralMetadata({
    schema: schema,
    propertyTables: propertyTables,
  });

  // Find the cartographic bounding box
  const cartographicMin = new Cartesian3(
    Number.POSITIVE_INFINITY,
    Number.POSITIVE_INFINITY,
    Number.POSITIVE_INFINITY
  );

  const cartographicMax = new Cartesian3(
    Number.NEGATIVE_INFINITY,
    Number.NEGATIVE_INFINITY,
    Number.NEGATIVE_INFINITY
  );

  for (let i = 0; i < featureCount; i++) {
    const feature = result.features[i];
    const linesLength = feature.lines.length;
    for (let j = 0; j < linesLength; j++) {
      const line = feature.lines[j];
      const positionsLength = line.length;
      for (let k = 0; k < positionsLength; k++) {
        Cartesian3.minimumByComponent(
          cartographicMin,
          line[k],
          cartographicMin
        );
        Cartesian3.maximumByComponent(
          cartographicMax,
          line[k],
          cartographicMax
        );
      }
    }
  }

  // Compute the ENU matrix
  const cartographicCenter = Cartesian3.midpoint(
    cartographicMin,
    cartographicMax,
    new Cartesian3()
  );
  const ecefCenter = Cartesian3.fromDegrees(
    cartographicCenter.x,
    cartographicCenter.y,
    cartographicCenter.z,
    Ellipsoid.WGS84,
    new Cartesian3()
  );
  const toGlobal = Transforms.eastNorthUpToFixedFrame(
    ecefCenter,
    Ellipsoid.WGS84,
    new Matrix4()
  );
  const toLocal = Matrix4.inverseTransformation(toGlobal, new Matrix4());

  // Count the number of vertices and indices
  let vertexCount = 0;
  let indexCount = 0;

  for (let i = 0; i < featureCount; i++) {
    const feature = result.features[i];
    const linesLength = feature.lines.length;
    for (let j = 0; j < linesLength; j++) {
      const line = feature.lines[j];
      vertexCount += line.length;
      indexCount += (line.length - 1) * 2;
    }
  }

  // Allocate typed arrays
  const positionsTypedArray = new Float32Array(vertexCount * 3);
  const featureIdsTypedArray = new Float32Array(vertexCount);
  const indicesTypedArray = IndexDatatype.createTypedArray(
    vertexCount,
    indexCount
  );
  const indexDatatype = IndexDatatype.fromTypedArray(indicesTypedArray);

  // Process the data. Convert positions to local ENU. Generate indices.
  const localMin = new Cartesian3(
    Number.POSITIVE_INFINITY,
    Number.POSITIVE_INFINITY,
    Number.POSITIVE_INFINITY
  );

  const localMax = new Cartesian3(
    Number.NEGATIVE_INFINITY,
    Number.NEGATIVE_INFINITY,
    Number.NEGATIVE_INFINITY
  );

  let vertexCounter = 0;
  let segmentCounter = 0;

  for (let i = 0; i < featureCount; i++) {
    const feature = result.features[i];
    const linesLength = feature.lines.length;
    for (let j = 0; j < linesLength; j++) {
      const line = feature.lines[j];
      const positionsLength = line.length;
      for (let k = 0; k < positionsLength; k++) {
        const cartographic = line[k];
        const globalCartesian = Cartesian3.fromDegrees(
          cartographic.x,
          cartographic.y,
          cartographic.z,
          Ellipsoid.WGS84,
          scratchCartesian
        );
        const localCartesian = Matrix4.multiplyByPoint(
          toLocal,
          globalCartesian,
          scratchCartesian
        );

        Cartesian3.minimumByComponent(localMin, localCartesian, localMin);
        Cartesian3.maximumByComponent(localMax, localCartesian, localMax);

        Cartesian3.pack(localCartesian, positionsTypedArray, vertexCounter * 3);

        featureIdsTypedArray[vertexCounter] = i;

        if (k < positionsLength - 1) {
          indicesTypedArray[segmentCounter * 2] = vertexCounter;
          indicesTypedArray[segmentCounter * 2 + 1] = vertexCounter + 1;
          segmentCounter++;
        }

        vertexCounter++;
      }
    }
  }

  // Create GPU buffers
  const positionBuffer = Buffer.createVertexBuffer({
    typedArray: positionsTypedArray,
    context: frameState.context,
    usage: BufferUsage.STATIC_DRAW,
  });
  positionBuffer.vertexArrayDestroyable = false;

  const featureIdBuffer = Buffer.createVertexBuffer({
    typedArray: featureIdsTypedArray,
    context: frameState.context,
    usage: BufferUsage.STATIC_DRAW,
  });
  featureIdBuffer.vertexArrayDestroyable = false;

  const indexBuffer = Buffer.createIndexBuffer({
    typedArray: indicesTypedArray,
    context: frameState.context,
    usage: BufferUsage.STATIC_DRAW,
    indexDatatype: indexDatatype,
  });
  indexBuffer.vertexArrayDestroyable = false;

  // Create ModelComponents
  const positionAttribute = new ModelComponents.Attribute();
  positionAttribute.semantic = VertexAttributeSemantic.POSITION;
  positionAttribute.componentDatatype = ComponentDatatype.FLOAT;
  positionAttribute.type = AttributeType.VEC3;
  positionAttribute.count = vertexCount;
  positionAttribute.min = localMin;
  positionAttribute.max = localMax;
  positionAttribute.buffer = positionBuffer;

  const featureIdAttribute = new ModelComponents.Attribute();
  featureIdAttribute.semantic = VertexAttributeSemantic.FEATURE_ID;
  featureIdAttribute.setIndex = 0;
  featureIdAttribute.componentDatatype = ComponentDatatype.FLOAT;
  featureIdAttribute.type = AttributeType.SCALAR;
  featureIdAttribute.count = vertexCount;
  featureIdAttribute.buffer = featureIdBuffer;

  const attributes = [positionAttribute, featureIdAttribute];

  const material = new ModelComponents.Material();
  material.unlit = true;

  const indices = new ModelComponents.Indices();
  indices.indexDatatype = indexDatatype;
  indices.count = indicesTypedArray.length;
  indices.buffer = indexBuffer;

  const featureId = new ModelComponents.FeatureIdAttribute();
  featureId.featureCount = featureCount;
  featureId.propertyTableId = 0;
  featureId.setIndex = 0;
  featureId.positionalLabel = "featureId_0";

  const featureIds = [featureId];

  const primitive = new ModelComponents.Primitive();
  primitive.attributes = attributes;
  primitive.indices = indices;
  primitive.featureIds = featureIds;
  primitive.primitiveType = PrimitiveType.LINES;
  primitive.material = material;

  const primitives = [primitive];

  const node = new ModelComponents.Node();
  node.index = 0;
  node.primitives = primitives;

  const nodes = [node];

  const scene = new ModelComponents.Scene();
  scene.nodes = nodes;

  const components = new ModelComponents.Components();
  components.scene = scene;
  components.nodes = nodes;
  components.transform = toGlobal;
  components.structuralMetadata = structuralMetadata;

  return components;
}

/**
 * Unloads the resource.
 * @private
 */
GeoJsonLoader.prototype.unload = function () {
  this._components = undefined;
};
