export {run} from '@oclif/command'

import bearing from "@turf/bearing";
import { bearingToAzimuth, Feature, lineString, LineString, Point, Position} from "@turf/helpers";
import { getCoord } from "@turf/invariant";
import length from "@turf/length";
import along from "@turf/along";
import BigNumber from "bignumber.js";
import { createHash } from "crypto";
import {
  FormOfWay, GISMetadata, LocationReference, OSMMetadata,
  SharedStreetsGeometry, SharedStreetsIntersection, SharedStreetsMetadata, SharedStreetsReference,
} from "sharedstreets-types";
import { isArray } from "util";

export { Graph, PathCandidate, GraphMode, ReferenceSideOfStreet} from './graph'
export { TileIndex } from './tile_index'
export { TilePathGroup, TilePath, TilePathParams, TileType } from './tiles'

/**
 * Shared Streets Java implementation
 *
 * @private
 * Intersection
 * https://github.com/sharedstreets/sharedstreets-builder/blob/master/src/main/java/io/sharedstreets/data/SharedStreetsIntersection.java#L42-L49
 *
 * Geometry
 * https://github.com/sharedstreets/sharedstreets-builder/blob/master/src/main/java/io/sharedstreets/data/SharedStreetsGeometry.java#L98-L108
 *
 * Reference
 * https://github.com/sharedstreets/sharedstreets-builder/blob/master/src/main/java/io/sharedstreets/data/SharedStreetsReference.java#L323-L340
 *
 * Location Reference
 * https://github.com/sharedstreets/sharedstreets-builder/blob/master/src/main/java/io/sharedstreets/data/SharedStreetsLocationReference.java
 *
 * OpenLR White Paper
 * http://www.openlr.org/data/docs/OpenLR-Whitepaper_v1.5.pdf
 */

/**
 * Geometry Id
 *
 * @param {Feature<LineString>|Array<Array<number>>} line Line Geometry as a GeoJSON LineString or an Array of Positions Array<<longitude, latitude>>.
 * @returns {string} SharedStreets Geometry Id
 * @example
 * const id = sharedstreets.geometryId([[110, 45], [115, 50], [120, 55]]);
 * id // => "ce9c0ec1472c0a8bab3190ab075e9b21"
 */
export function geometryId(line: Feature<LineString> | LineString | number[][]): string {
  const message = geometryMessage(line);
  return generateHash(message);
}

/**
 * Geometry Message
 *
 * @private
 */
export function geometryMessage(line: Feature<LineString> | LineString | number[][]): string {
  const coords = getCoords(line);
  return "Geometry " + coords.map(([x, y]) => `${round(x)} ${round(y)}`).join(" ");
}

/**
 * geometry
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString Feature
 * @param {Object} [options={}] Optional parameters
 * @param {string} [options.formOfWay] Property field that contains FormOfWay value (number/string).
 * @param {string} [options.roadClass] Property field that contains RoadClass value (number/string).
 * @returns {SharedStreetsGeometry} SharedStreets Geometry
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const geom = sharedstreets.geometry(line);
 * geom.id; // => "ce9c0ec1472c0a8bab3190ab075e9b21"
 * geom.lonlats; // => [ 110, 45, 115, 50, 120, 55 ]
 */
export function geometry(line: Feature<LineString>, options: {
  formOfWay?: string,
  roadClass?: string,
  // To-Do
  // - fromIntersection [optional]
  // - toIntersection [optional]
  // - forwardReference [optional]
  // - backReference [optional]
} = {}): SharedStreetsGeometry {
  let properties: any = {};
  const coords = getCoords(line);

  // Extract Properties from GeoJSON LineString Feature
  if (!isArray(line) && line.type === "Feature") { properties = line.properties || {}; }


  // FormOfWay needs to be extracted from the GeoJSON properties.
  let formOfWay = 0;
  if (options.formOfWay && properties[options.formOfWay]) {
    formOfWay = properties[options.formOfWay];
  }

  // RoadClass needs to be extracted from the GeoJSON properties.
  let roadClass = "Other";
  if (options.roadClass && properties[options.roadClass]) {
    roadClass = properties[options.roadClass];
  }
  const forwardRef = forwardReference(line, {formOfWay:formOfWay});
  const backRef = backReference(line, {formOfWay:formOfWay});
  const forwardReferenceId = forwardRef.id;
  const backReferenceId = backRef.id;

  const fromIntersectionId = forwardRef.locationReferences[0].intersectionId;
  const toIntersectionId = forwardRef.locationReferences[forwardRef.locationReferences.length-1].intersectionId;

  // Save Results
  const id = geometryId(line);
  const lonlats = coordsToLonlats(coords);

  return {
    id,
    fromIntersectionId,
    toIntersectionId,
    forwardReferenceId,
    backReferenceId,
    roadClass,
    lonlats,
  };
}

/**
 * Intersection Id
 *
 * @param {Feature<Point>|Array<number>} pt Point location reference as a GeoJSON Point or an Array of numbers <longitude, latitude>.
 * @returns {string} SharedStreets Intersection Id
 * @example
 * const id = sharedstreets.intersectionId([110, 45]);
 * id // => "71f34691f182a467137b3d37265cb3b6"
 */
export function intersectionId(pt: number[] | Feature<Point> | Point, id?:string | number): string {
  const message = intersectionMessage(pt, id);
  return generateHash(message);
}

/**
 * Intersection Message
 *
 * @private
 */
export function intersectionMessage(pt: number[] | Feature<Point> | Point, id?:string | number): string {
  const [lon, lat] = getCoord(pt);
  var message = `Intersection ${round(lon)} ${round(lat)}`;
  if(id != undefined)
    message = message + ' ' + id;
  return message

}

/**
 * Intersection
 *
 * @param {Feature<Point>|Array<number>} pt Point location reference as a GeoJSON Point or an Array of numbers <longitude, latitude>.
 * @param {Object} [options={}] Optional parameters
 * @param {string} [options.nodeId] Define NodeId for Intersection
 * @returns {SharedStreetsIntersection} SharedStreets Intersection
 * @example
 * const intersection = sharedstreets.intersection([110, 45]);
 * intersection.id // => "71f34691f182a467137b3d37265cb3b6"
 */
export function intersection(pt: number[] | Feature<Point> | Point, options: {
  nodeId?: number | string,
  inboundReferences?: LocationReference[],
  outboundReferencesIds?: LocationReference[],
} = {}): SharedStreetsIntersection {
  // Default params
  const inboundReferences = options.inboundReferences || [];
  const outboundReferences = options.outboundReferencesIds || [];
  const nodeId = options.nodeId;

  // Main
  const [lon, lat] = getCoord(pt);
  const id = intersectionId(pt, nodeId);
  const inboundReferenceIds = inboundReferences.map((ref) => ref.intersectionId);
  const outboundReferenceIds = outboundReferences.map((ref) => ref.intersectionId);

  const data: SharedStreetsIntersection = {
    id,
    lon,
    lat,
    inboundReferenceIds,
    outboundReferenceIds,
  };
  if (nodeId !== undefined) { data.nodeId = nodeId; }
  return data;
}

/**
 * Reference Id
 *
 * @param {Array<LocationReference>} locationReferences An Array of Location References.
 * @param {FormOfWay} [formOfWay=0] Form Of Way
 * @returns {string} SharedStreets Reference Id
 * @example
 * const locationReferences = [
 *   sharedstreets.locationReference([-74.0048213, 40.7416415], {outboundBearing: 208, distanceToNextRef: 9279}),
 *   sharedstreets.locationReference([-74.0051265, 40.7408505], {inboundBearing: 188})
 * ];
 * const formOfWay = 2; // => "MultipleCarriageway"
 *
 * const id = sharedstreets.referenceId(locationReferences, formOfWay);
 * id // => "ef209661aeebadfb4e0a2cb93153493f"
 */
export function referenceId(
  locationReferences: LocationReference[],
  formOfWay = FormOfWay.Undefined,
): string {
  const message = referenceMessage(locationReferences, formOfWay);
  const hash = generateHash(message);
  return hash;
}

/**
 * Reference Message
 *
 * @private
 */
export function referenceMessage(
  locationReferences: LocationReference[],
  formOfWay = FormOfWay.Undefined,
): string {
  // Convert FormOfWay to Number if encoding ID
  if (typeof formOfWay !== "number") { formOfWay = getFormOfWayNumber(formOfWay); }

  let message = `Reference ${formOfWay}`;
  locationReferences.forEach((lr) => {
    message += ` ${round(lr.lon)} ${round(lr.lat)}`;
    if (lr.outboundBearing !== null && lr.outboundBearing !== undefined &&
        lr.distanceToNextRef !== null && lr.distanceToNextRef !== undefined) {
      message += ` ${Math.round(lr.outboundBearing)}`;
      message += ` ${Math.round(Math.round(lr.distanceToNextRef / 100))}`; // distanceToNextRef  stored in centimeter but using meters to compute ref Id
    }
  });
  return message;
}

/**
 * Reference
 *
 * @param {SharedStreetsGeometry} geom SharedStreets Geometry
 * @param {Array<LocationReference>} locationReferences An Array of Location References.
 * @param {number} [formOfWay=0] Form Of Way (default Undefined)
 * @returns {SharedStreetsReference} SharedStreets Reference
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const geom = sharedstreets.geometry(line);
 * const locationReferences = [
 *   sharedstreets.locationReference([-74.0048213, 40.7416415], {outboundBearing: 208, distanceToNextRef: 9279}),
 *   sharedstreets.locationReference([-74.0051265, 40.7408505], {inboundBearing: 188}),
 * ];
 * const formOfWay = 2; // => "MultipleCarriageway"
 * const ref = sharedstreets.reference(geom, locationReferences, formOfWay);
 * ref.id // => "ef209661aeebadfb4e0a2cb93153493f"
 */
export function reference(
  geom: SharedStreetsGeometry,
  locationReferences: LocationReference[],
  formOfWay = FormOfWay.Undefined,
): SharedStreetsReference {
  return {
    id: referenceId(locationReferences, formOfWay),
    geometryId: geom.id,
    formOfWay,
    locationReferences,
  };
}

/**
 * Forward Reference
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString Feature or an Array of Positions
 * @param {Object} [options={}] Optional parameters
 * @param {number|string} [options.formOfWay=0] Form of Way (default "Undefined")
 * @returns {SharedStreetsReference} Forward SharedStreets Reference
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const ref = sharedstreets.forwardReference(line);
 * ref.id // => "3f652e4585aa7d7df3c1fbe4f55cea0a"
 */
export function forwardReference (
  line: Feature<LineString>,
  options: {
    formOfWay?: number|string,
  } = {},
) : SharedStreetsReference {

  const lineLength = Math.round(length(line, {units: "meters"}) * 100);

  const formOfWay = getFormOfWay(line, options);
  const geomId = geometryId(line);

  // lines over 15 are divided into smaller segments
  const MAX_SEGMENT_LENGTH = 15000;
  var segmentCount = Math.ceil(lineLength / MAX_SEGMENT_LENGTH);

  var locationReferences = [];

  for(var i = 0; i < segmentCount + 1; i++){
    var refProperties:{
      outboundBearing?:number,
      inboundBearing?:number,
      distanceToNextRef?:number } = {};

    if(i < segmentCount){
      refProperties.outboundBearing = outboundBearing(line, lineLength, i * (lineLength / segmentCount));
      refProperties.distanceToNextRef = Math.round((lineLength / segmentCount) * 100);
    }
    if(i > 0){
      refProperties.inboundBearing = inboundBearing(line, lineLength, i * (lineLength / segmentCount));
    }

    var lrCoord;

    if(i == 0)
      lrCoord = getStartCoord(line);
    else if(i == segmentCount)
      lrCoord = getEndCoord(line);
    else {
      var pos = i * (lineLength / segmentCount);
      var pt = along(line, pos);
      lrCoord = getCoord(pt);
    }

    var ref = locationReference(lrCoord, refProperties);
    locationReferences.push(ref);
  }

  const id = referenceId(locationReferences, formOfWay);

  return {
    id,
    geometryId: geomId,
    formOfWay,
    locationReferences
  };
}

/**
 * Back Reference
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString Feature or an Array of Positions
 * @param {Object} [options={}] Optional parameters
 * @param {number|string} [options.formOfWay=0] Form of Way (default "Undefined")
 * @returns {SharedStreetsReference} Back SharedStreets Reference
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const ref = sharedstreets.backReference(line);
 * ref.id // => "a18b2674e41cad630f5693154837baf4"
 */
export function backReference(
  line: Feature<LineString>,
  options: {
    formOfWay?: number|string,
  } = {},
): SharedStreetsReference {
    var geomId = geometryId(line);
    var reversedLine = JSON.parse(JSON.stringify(line));
    reversedLine.geometry.coordinates.reverse();
    var ref = forwardReference(reversedLine,  options);
    ref.geometryId = geomId;
    return ref;
}

/**
 * Location Reference
 *
 * @private
 * @param {Feature<Point>|Array<number>} pt Point as a GeoJSON Point or an Array of numbers <longitude, latitude>.
 * @param {Object} [options={}] Optional parameters
 * @param {string} [options.intersectionId] Intersection Id - Fallbacks to input's point `id` or generates Intersection Id.
 * @param {number} [options.inboundBearing] Inbound bearing of the street geometry for the 20 meters immediately following the location reference.
 * @param {number} [options.outboundBearing] Outbound bearing.
 * @param {number} [options.distanceToNextRef] Distance to next Location Reference (distance must be defined in centimeters).
 * @returns {LocationReference} SharedStreets Location Reference
 * @example
 * const options = {
 *   outboundBearing: 208,
 *   distanceToNextRef: 9279
 * };
 * const locRef = sharedstreets.locationReference([-74.00482177734375, 40.741641998291016], options);
 * locRef.intersectionId // => "5c88d4fa3900a083355c46c54da8f584"
 */
export function locationReference(
  pt: number[] | Feature<Point> | Point,
  options: {
    intersectionId?: string,
    inboundBearing?: number,
    outboundBearing?: number,
    distanceToNextRef?: number,
} = {}): LocationReference {
  const coord = getCoord(pt);
  const id = options.intersectionId || intersectionId(coord);

  // Include extra properties & Reference ID to GeoJSON Properties
  const locRef: LocationReference = {
    intersectionId: id,
    lat: coord[1],
    lon: coord[0],
  };
  if (options.inboundBearing !== undefined) { locRef.inboundBearing = options.inboundBearing; }
  if (options.outboundBearing !== undefined) { locRef.outboundBearing = options.outboundBearing; }
  if (options.distanceToNextRef !== undefined) { locRef.distanceToNextRef = options.distanceToNextRef; }

  if (locRef.outboundBearing !== undefined && locRef.distanceToNextRef === undefined) {
    throw new Error("distanceToNextRef is required if outboundBearing is present");
  }
  return locRef;
}

/**
 * Metadata
 *
 * @param {SharedStreetsGeometry} geom SharedStreets Geometry
 * @param {OSMMetadata} [osmMetadata={}] OSM Metadata
 * @param {Array<GISMetadata>} [gisMetadata=[]] GIS Metadata
 * @param {}
 * @returns {SharedStreetsMetadata} SharedStreets Metadata
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const geom = sharedstreets.geometry(line);
 * const metadata = sharedstreets.metadata(geom)
 */
export function metadata(
  geom: SharedStreetsGeometry,
  osmMetadata: OSMMetadata = {},
  gisMetadata: GISMetadata[] = [],
): SharedStreetsMetadata {
  return {
    geometryId: geom.id,
    osmMetadata,
    gisMetadata,
  };
}

/**
 * Calculates outbound bearing from a LineString
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString or an Array of Positions
 * @param {number} len length of line
 * @param {number} dist distance along line to sample outbound bearing
 * @returns {number} Outbound Bearing
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const outboundBearing = sharedstreets.outboundBearing(line);
 * if line is less than 20 meters long bearing is from start to end of line
 * outboundBearing; // => 208
 */
export function outboundBearing(line: Feature<LineString>, len:number, dist:number): number {
  // LRs describe the compass bearing of the street geometry for the 20 meters immediately following the LR.
  if(len > 20) {
    const start = along(line, dist, {units: "meters"});
    const end = along(line, dist + 20, {units: "meters"});
    // Calculate outbound & inbound
    return bearingToAzimuth(Math.round(bearing(start, end)));
  }
  else {
    const start = along(line, 0, {units: "meters"});
    const end = along(line, len, {units: "meters"});
    // Calculate outbound & inbound
    return bearingToAzimuth(Math.round(bearing(start, end)));
  }

}

/**
 * Calculates inbound bearing from a LineString
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString or an Array of Positions
 * @param {number} len length of line
 * @param {number} dist distance along line to sample outbound bearing
 * @returns {number} Inbound Bearing
 * @example
 * const line = [[110, 45], [115, 50], [120, 55]];
 * const inboundBearing = sharedstreets.inboundBearing(line);
 * if line is less than 20 meters long bearing is from start to end of line
 * inboundBearing; // => 188
 */
export function inboundBearing(line: Feature<LineString>, len:number, dist:number): number {
  if(len > 20) {
    const start = along(line, dist - 20, {units: "meters"});
    const end = along(line, dist, {units: "meters"});

    return bearingToAzimuth(Math.round(bearing(start, end)));
  }
  else  {
    const start = along(line, 0, {units: "meters"});
    const end = along(line, len, {units: "meters"});

    return bearingToAzimuth(Math.round(bearing(start, end)));
  }

}

/**
 * Calculates inbound bearing from a LineString
 *
 * @param {Coord} start GeoJSON Point or an Array of numbers [Longitude/Latitude]
 * @param {Coord} end GeoJSON Point or an Array of numbers [Longitude/Latitude]
 * @returns {number} Distance to next Ref in centimeters
 * @example
 * const start = [110, 45];
 * const end = [120, 55];
 * const distanceToNextRef = sharedstreets.distanceToNextRef(start, end);
 * distanceToNextRef; // => 9279
 */
export function distanceToNextRef(line: Feature<LineString>): number {
  if (Array.isArray(line)) { line = lineString(line); }
  return Math.round(length(line, {units: "meters"}) * 100);
}

/**
 * Converts lonlats to GeoJSON LineString Coords
 *
 * @param {Array<number>} lonlats Single Array of paired longitudes & latitude
 * @returns {Array<Array<number>>}  GeoJSON LineString coordinates
 * @example
 * const coords = lonlatsToCoords([110, 45, 120, 55]);
 * coords // => [[110, 45], [120, 55]]
 */
export function lonlatsToCoords(lonlats: number[]) {
  const coords: number[][] = [];
  lonlats.reduce((lon, deg, index) => {
    if (index % 2 === 0) { return deg; } // Longitude
    coords.push([lon, deg]);
    return deg; // Latitude
  });
  return coords;
}

/**
 * Converts GeoJSON LineString Coords to lonlats
 *
 * @param {Array<Array<number, number>>} coords GeoJSON LineString coordinates
 * @returns {Array<number>} lonlats Single Array of paired longitudes & latitude
 * @example
 * const lonlats = coordsToLonlats([[110, 45], [120, 55]]);
 * lonlats // => [110, 45, 120, 55]
 */
export function coordsToLonlats(coords: number[][]) {
  const lonlats: number[] = [];
  coords.forEach((coord) => {
    lonlats.push(coord[0], coord[1]);
  });
  return lonlats;
}

/**
 * Generates Base16 Hash
 *
 * @param {string} message Message to hash
 * @returns {string} SharedStreets Reference ID
 * @example
 * const message = "Intersection -74.00482177734375 40.741641998291016";
 * const hash = sharedstreets.generateHash(message);
 * hash // => "69f13f881649cb21ee3b359730790bb9"
 */
export function generateHash(message: string): string {
  return createHash("md5").update(message).digest("hex");
}

/**
 * Get RoadClass from a Number to a String
 *
 * @param {number} value Number value [between 0-8]
 * @returns {string} Road Class
 * @example
 * sharedstreets.getRoadClassString(0); // => "Motorway"
 * sharedstreets.getRoadClassString(5); // => "Residential"
 */
export function getRoadClassString(value: number) {
  switch (value) {
    case 0: return "Motorway";
    case 1: return "Trunk";
    case 2: return "Primary";
    case 3: return "Secondary";
    case 4: return "Tertiary";
    case 5: return "Residential";
    case 6: return "Unclassified";
    case 7: return "Service";
    case 8: return "Other";
    default: throw new Error(`[${value}] unknown RoadClass Number value`);
  }
}

/**
 * Get RoadClass from a String to a Number
 *
 * @param {number} value String value ["Motorway", "Trunk", "Primary", etc...]
 * @returns {string} Road Class
 * @example
 * sharedstreets.getRoadClassNumber("Motorway"); // => 0
 * sharedstreets.getRoadClassNumber("Residential"); // => 5
 */
export function getRoadClassNumber(value: string) {
  switch (value) {
    case "Motorway": return 0;
    case "Trunk": return 1;
    case "Primary": return 2;
    case "Secondary": return 3;
    case "Tertiary": return 4;
    case "Residential": return 5;
    case "Unclassified": return 6;
    case "Service": return 7;
    case "Other": return 8;
    default: throw new Error(`[${value}] unknown RoadClass String value`);
  }
}

/**
 * Get FormOfWay from a GeoJSON LineString and/or Optional parameters
 *
 * @param {number} value Number value [between 0-7]
 * @returns {string} Form of Way
 * @example
 * sharedstreets.getFormOfWayString(0); // => "Undefined"
 * sharedstreets.getFormOfWayString(5); // => "TrafficSquare"
 */
export function getFormOfWayString(value: number): string {
  switch (value) {
    case undefined:
    case 0: return "Undefined";
    case 1: return "Motorway";
    case 2: return "MultipleCarriageway";
    case 3: return "SingleCarriageway";
    case 4: return "Roundabout";
    case 5: return "TrafficSquare";
    case 6: return "SlipRoad";
    case 7: return "Other";
    default: throw new Error(`[${value}] unknown FormOfWay Number value`);
  }
}

/**
 * Get FormOfWay from a GeoJSON LineString
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString Feature or an Array of Positions
 * @param {Object} [options={}] Optional parameters
 * @param {number} [options.formOfWay=0] Form of Way
 * @example
 * const lineA = turf.lineString([[110, 45], [115, 50], [120, 55]], {formOfWay: 3});
 * const lineB = turf.lineString([[110, 45], [115, 50], [120, 55]]);
 * const lineC = turf.lineString([[110, 45], [115, 50], [120, 55]], {formOfWay: "Motorway"});
 *
 * sharedstreets.getFormOfWay(lineA); // => 3
 * sharedstreets.getFormOfWay(lineB); // => 0
 * sharedstreets.getFormOfWay(lineC); // => 1
 */
export function getFormOfWay(
  line: Feature<LineString> | LineString | Position[],
  options: {
    formOfWay?: number | string,
  } = {},
): number {
  // Set default to Other (0)
  let formOfWay: number | string = FormOfWay.Undefined;

  // Retrieve formOfWay from Optional Parameters (priority order since it is user defined)
  if (options.formOfWay !== undefined) {
    formOfWay = options.formOfWay;

    // Retrieve formOfWay via GeoJSON LineString properties
  } else if (!Array.isArray(line) && line.type === "Feature" && line.properties && line.properties.formOfWay) {
    formOfWay = line.properties.formOfWay;
  }

  // Assert value to Number
  if (typeof formOfWay === "string") { formOfWay = getFormOfWayNumber(formOfWay); }

  return formOfWay;
}

/**
 * Get Start Coordinate from a GeoJSON LineString
 *
 * @param {Feature<LineString>|Array<Position>} line GeoJSON LineString or an Array of Positiosn
 * @returns {Position} Start Coordinate
 * @example
 * const line = turf.lineString([[110, 45], [115, 50], [120, 55]]);
 * const start = sharedstreets.getStartCoord(line);
 * start // => [110, 45]
 */
export function getStartCoord(line: Feature<LineString> | LineString | Position[]): Position {
  // Array of Positions
  if (Array.isArray(line)) {
    return line[0];
  // GeoJSON Feature
  } else if (line.type === "Feature" && line.geometry) {
    return line.geometry.coordinates[0];
  // GeoJSON Geometry
  } else if (line.type === "LineString") {
    return line.coordinates[0];
  } else {
    throw new Error("invalid line");
  }
}

/**
 * Get Start Coordinate from a GeoJSON LineString
 *
 * @param {Feature<LineString>|Array<Position>} line GeoJSON LineString or an Array of Positiosn
 * @returns {Position} Start Coordinate
 * @example
 * const line = turf.lineString([[110, 45], [115, 50], [120, 55]]);
 * const end = sharedstreets.getEndCoord(line);
 * end // => [120, 55]
 */
export function getEndCoord(line: Feature<LineString> | LineString | Position[]): Position {
  // Array of Positions
  if (Array.isArray(line)) {
    return line[line.length - 1];
  // GeoJSON Feature
  } else if (line.type === "Feature" && line.geometry) {
    return line.geometry.coordinates[line.geometry.coordinates.length - 1];
  // GeoJSON Geometry
  } else if (line.type === "LineString") {
    return line.coordinates[line.coordinates.length - 1];
  } else {
    throw new Error("invalid line");
  }
}

/**
 * Get FormOfWay from a String to a Number
 *
 * @param {number} value String value [ex: "Undefined", "Motorway", etc...]
 * @returns {number} Form of Way
 * @example
 * sharedstreets.getFormOfWayNumber("Undefined"); // => 0
 * sharedstreets.getFormOfWayNumber("TrafficSquare"); // => 5
 */
export function getFormOfWayNumber(value: string) {
  switch (value) {
    case undefined:
    case "Undefined": return 0;
    case "Motorway": return 1;
    case "MultipleCarriageway": return 2;
    case "SingleCarriageway": return 3;
    case "Roundabout": return 4;
    case "TrafficSquare": return 5;
    case "SlipRoad": return 6;
    case "Other": return 7;
    default: throw new Error(`[${value}] unknown FormOfWay String value`);
  }
}

/**
 * getCoords
 *
 * @param {Feature<LineString>|Array<Array<number>>} line GeoJSON LineString or an Array of positions
 * @returns {Array<Array<number>>} Array of positions
 * @example
 * const line = turf.lineString([[110, 45], [115, 50], [120, 55]]);
 * const coords = sharedstreets.getCoords(line);
 * coords; // => [[110, 45], [115, 50], [120, 55]]
 */
export function getCoords(line: Feature<LineString> | LineString | number[][]): number[][] {
  // Deconstruct GeoJSON LineString
  let coords: number[][];
  if (isArray(line)) {
    coords = line;
  } else if (line.type === "Feature") {
    if (line.geometry === null) { throw new Error("line geometry cannot be null"); }
    coords = line.geometry.coordinates;
  } else {
    coords = line.coordinates;
  }
  return coords;
}

/**
 * Round Number to 5 decimals
 *
 * @param {number} num Number to round
 * @param {number} [decimalPlaces=5] Decimal Places
 * @returns {string} Big Number fixed string
 * @example
 * sharedstreets.round(10.123456789) // => 10.123457
 */

export function round(num: number, decimalPlaces = 5): string {
  return new BigNumber(String(num)).toFixed(decimalPlaces);
}
