// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {log} from '@deck.gl/core';
import type {
  Feature,
  GeoJSON,
  GeoJsonGeometryTypes,
  LineString,
  MultiLineString,
  MultiPoint,
  MultiPolygon,
  Point,
  Polygon
} from 'geojson';

type SupportedGeometry = Point | MultiPoint | LineString | MultiLineString | Polygon | MultiPolygon;

export type SeparatedGeometries = {
  pointFeatures: {geometry: Point}[];
  lineFeatures: {geometry: LineString}[];
  polygonFeatures: {geometry: Polygon}[];
  polygonOutlineFeatures: {geometry: LineString}[];
};

/**
 * "Normalizes" complete or partial GeoJSON data into iterable list of features
 * Can accept GeoJSON geometry or "Feature", "FeatureCollection" in addition
 * to plain arrays and iterables.
 * Works by extracting the feature array or wrapping single objects in an array,
 * so that subsequent code can simply iterate over features.
 *
 * @param {object} geojson - geojson data
 * @param {Object|Array} data - geojson object (FeatureCollection, Feature or
 *  Geometry) or array of features
 * @return {Array|"iteratable"} - iterable list of features
 */
export function getGeojsonFeatures(geojson: GeoJSON): Feature[] {
  // If array, assume this is a list of features
  if (Array.isArray(geojson)) {
    return geojson;
  }

  log.assert(geojson.type, 'GeoJSON does not have type');

  switch (geojson.type) {
    case 'Feature':
      // Wrap the feature in a 'Features' array
      return [geojson];
    case 'FeatureCollection':
      // Just return the 'Features' array from the collection
      log.assert(Array.isArray(geojson.features), 'GeoJSON does not have features array');
      return geojson.features;
    default:
      // Assume it's a geometry, we'll check type in separateGeojsonFeatures
      // Wrap the geometry object in a 'Feature' object and wrap in an array
      return [{geometry: geojson}] as Feature[];
  }
}

// Linearize
export function separateGeojsonFeatures(
  features: Feature[],
  wrapFeature: <T>(row: T, sourceObject: any, sourceObjectIndex: number) => T,
  dataRange: {startRow?: number; endRow?: number} = {}
): SeparatedGeometries {
  const separated: SeparatedGeometries = {
    pointFeatures: [],
    lineFeatures: [],
    polygonFeatures: [],
    polygonOutlineFeatures: []
  };
  const {startRow = 0, endRow = features.length} = dataRange;

  for (let featureIndex = startRow; featureIndex < endRow; featureIndex++) {
    const feature = features[featureIndex];
    const {geometry} = feature;

    if (!geometry) {
      // geometry can be null per specification
      continue; // eslint-disable-line no-continue
    }

    if (geometry.type === 'GeometryCollection') {
      log.assert(Array.isArray(geometry.geometries), 'GeoJSON does not have geometries array');
      const {geometries} = geometry;
      for (let i = 0; i < geometries.length; i++) {
        const subGeometry = geometries[i];
        separateGeometry(
          subGeometry as SupportedGeometry,
          separated,
          wrapFeature,
          feature,
          featureIndex
        );
      }
    } else {
      separateGeometry(geometry, separated, wrapFeature, feature, featureIndex);
    }
  }

  return separated;
}

function separateGeometry(
  geometry: SupportedGeometry,
  separated: SeparatedGeometries,
  wrapFeature: <T>(row: T, sourceObject: any, sourceObjectIndex: number) => T,
  sourceFeature: Feature,
  sourceFeatureIndex: number
) {
  const {type, coordinates} = geometry;
  const {pointFeatures, lineFeatures, polygonFeatures, polygonOutlineFeatures} = separated;

  if (!validateGeometry(type, coordinates)) {
    // Avoid hard failure if some features are malformed
    log.warn(`${type} coordinates are malformed`)();
    return;
  }

  // Split each feature, but keep track of the source feature and index (for Multi* geometries)
  switch (type) {
    case 'Point':
      pointFeatures.push(
        wrapFeature(
          {
            geometry
          },
          sourceFeature,
          sourceFeatureIndex
        )
      );
      break;
    case 'MultiPoint':
      coordinates.forEach(point => {
        pointFeatures.push(
          wrapFeature(
            {
              geometry: {type: 'Point', coordinates: point}
            },
            sourceFeature,
            sourceFeatureIndex
          )
        );
      });
      break;
    case 'LineString':
      lineFeatures.push(
        wrapFeature(
          {
            geometry
          },
          sourceFeature,
          sourceFeatureIndex
        )
      );
      break;
    case 'MultiLineString':
      // Break multilinestrings into multiple lines
      coordinates.forEach(path => {
        lineFeatures.push(
          wrapFeature(
            {
              geometry: {type: 'LineString', coordinates: path}
            },
            sourceFeature,
            sourceFeatureIndex
          )
        );
      });
      break;
    case 'Polygon':
      polygonFeatures.push(
        wrapFeature(
          {
            geometry
          },
          sourceFeature,
          sourceFeatureIndex
        )
      );
      // Break polygon into multiple lines
      coordinates.forEach(path => {
        polygonOutlineFeatures.push(
          wrapFeature(
            {
              geometry: {type: 'LineString', coordinates: path}
            },
            sourceFeature,
            sourceFeatureIndex
          )
        );
      });
      break;
    case 'MultiPolygon':
      // Break multipolygons into multiple polygons
      coordinates.forEach(polygon => {
        polygonFeatures.push(
          wrapFeature(
            {
              geometry: {type: 'Polygon', coordinates: polygon}
            },
            sourceFeature,
            sourceFeatureIndex
          )
        );
        // Break polygon into multiple lines
        polygon.forEach(path => {
          polygonOutlineFeatures.push(
            wrapFeature(
              {
                geometry: {type: 'LineString', coordinates: path}
              },
              sourceFeature,
              sourceFeatureIndex
            )
          );
        });
      });
      break;
    default:
  }
}

/**
 * Simple GeoJSON validation util. For perf reasons we do not validate against the full spec,
 * only the following:
   - geometry.type is supported
   - geometry.coordinate has correct nesting level
 */
const COORDINATE_NEST_LEVEL: Record<SupportedGeometry['type'], number> = {
  Point: 1,
  MultiPoint: 2,
  LineString: 2,
  MultiLineString: 3,
  Polygon: 3,
  MultiPolygon: 4
};

export function validateGeometry(type: GeoJsonGeometryTypes, coordinates: any): boolean {
  let nestLevel = COORDINATE_NEST_LEVEL[type] as number;

  log.assert(nestLevel, `Unknown GeoJSON type ${type}`);

  while (coordinates && --nestLevel > 0) {
    coordinates = coordinates[0];
  }

  return coordinates && Number.isFinite(coordinates[0]);
}
