text: let proj4 = require('proj4');
  proj4 = proj4.default ? proj4.default : proj4;
  
  const terraformer = require('terraformer');
  const teraProj = require('terraformer-proj4js');
  
  /**
   * Reproject a GeoJSON object in place.  This is a wrapper around terraformer-proj4js.
   * @param {Object} geojson the GeoJSON to be reprojected, this will be modified in place
   * @param {String|Number} outputSpatialReference the target spatial reference,
   * 'EPSG:4326' is used by default; if a number is suppied it will be used as an EPSG code
   * @param {String|Number} inputSpatialReference same rules as outputSpatialReference if suppied
   * if missing it will attempt to find it encoded in the GeoJSON
   */
  function projectGeojson(geojson, outputSpatialReference, inputSpatialReference) {
      const converter = teraProj(terraformer, proj4);
      converter(geojson, outputSpatialReference, inputSpatialReference);
  }
  
  /**
   * Convert a projection to an string that is compatible with proj4.  If it is an ESRI SpatialReference or an integer it will be converted.
   * @param {Object|Integer|String} proj an ESRI SpatialReference, integer or string.  Strings will be unchanged and unchecked,
   * ints and SpatialReference objects will be converted.
   * @return {String} A string in the form EPSG:####
   * @private
   */
  function normalizeProj(proj) {
      if (typeof proj === 'object') {
          if (proj.wkid) {
              return 'EPSG:' + proj.wkid;
          } else if (proj.wkt) {
              return proj.wkt;
          }
      } else if (typeof proj === 'number') {
          return 'EPSG:' + proj;
      } else if (typeof proj === 'string') {
          return proj;
      }
      throw new Error('Bad argument type, please provide a string, integer or SpatialReference object.');
  }
  
  /**
   * Project a single point.
   * @param {Object|Integer|String} srcProj the spatial reference of the point (as ESRI SpatialReference, integer WKID or an EPSG string)
   * @param {Object|Integer|String} destProj the spatial reference of the result (as ESRI SpatialReference, integer WKID or an EPSG string)
   * @param {Array|Object} point a 2d array or object with {x,y} props containing the coordinates to Reproject
   * @return {Array|Object} a 2d array or object containing the projected point
   */
  function localProjectPoint(srcProj, destProj, point) {
      return proj4(normalizeProj(srcProj), normalizeProj(destProj), point);
  }
  
  /**
   * Project a single point.
   * @param {Object|Integer|String} destProj the spatial reference of the result (as ESRI SpatialReference, integer WKID or an EPSG string)
   * @param {Object} geometry an object conforming to ESRI Geometry object standards containing the coordinates to Reproject
   * @return {Object} an object conforming to ESRI Geomtery object standards containing the input geometry in the destination projection
   */
  function localProjectGeometryBuilder(esriBundle) {
      return (destProj, geometry) => {
          // HACK >:'(
          // terraformer has this undesired behavior where, if your input geometry is in WKID 102100, it will magically
          // project all your co-ordinates to lat/long when converting between ESRI and GeoJSON formats.
          // to stop it from ruining us, we temporarily set the spatial reference to nonsense so it will leave it alone
          const realSR = geometry.spatialReference;
          geometry.spatialReference = { wkid: 8888 }; // nonsense!
          const grGeoJ = terraformer.ArcGIS.parse(geometry, { sr: 8888 });
          geometry.spatialReference = realSR;
  
          // project json
          projectGeojson(grGeoJ, normalizeProj(destProj), normalizeProj(realSR));
  
          // back to esri format
          const grEsri = terraformer.ArcGIS.convert(grGeoJ);
  
          // doing this because .convert likes to attach a lat/long spatial reference for fun.
          grEsri.spatialReference = new esriBundle.SpatialReference(destProj);
  
          return grEsri;
      };
  }
  
  /**
   * Reproject an EsriExtent object on the client.  Does not require network
   * traffic, but may not handle conversion between projection types as well.
   * Internally it tests 8 points along each edge and takes the max extent
   * of the result.
   *
   * @param {EsriExtent} extent to reproject
   * @param {Object} sr is the target spatial reference (if a number it
   *                 will be treated as a WKID)
   * @returns {Object} an extent as an unstructured object
   */
  function localProjectExtent(extent, sr) {
      // interpolates two points by splitting the line in half recursively
      function interpolate(p0, p1, steps) {
          if (steps === 0) {
              return [p0, p1];
          }
  
          let mid = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
          if (steps === 1) {
              return [p0, mid, p1];
          }
          if (steps > 1) {
              let i0 = interpolate(p0, mid, steps - 1);
              let i1 = interpolate(mid, p1, steps - 1);
              return i0.concat(i1.slice(1));
          }
      }
  
      const points = [
          [extent.xmin, extent.ymin],
          [extent.xmax, extent.ymin],
          [extent.xmax, extent.ymax],
          [extent.xmin, extent.ymax],
          [extent.xmin, extent.ymin],
      ];
      let interpolatedPoly = [];
      let srcProj;
  
      // interpolate each edge by splitting it in half 3 times (since lines are not guaranteed to project to lines we need to consider
      // max / min points in the middle of line segments)
      [0, 1, 2, 3]
          .map((i) => interpolate(points[i], points[i + 1], 3).slice(1))
          .forEach((seg) => (interpolatedPoly = interpolatedPoly.concat(seg)));
  
      // find the source extent (either from wkid or wkt)
      srcProj = normalizeProj(extent.spatialReference);
  
      // find the destination extent
      let destProj = normalizeProj(sr);
  
      if (extent.spatialReference.wkid && !proj4.defs(srcProj)) {
          throw new Error('Source projection WKID not recognized by proj4 library');
      }
      const projConvert = proj4(srcProj, destProj);
      const transformed = interpolatedPoly.map((x) => projConvert.forward(x));
  
      const xvals = transformed.map((x) => x[0]);
      const yvals = transformed.map((x) => x[1]);
  
      const x0 = Math.min.apply(null, xvals);
      const x1 = Math.max.apply(null, xvals);
  
      const y0 = Math.min.apply(null, yvals);
      const y1 = Math.max.apply(null, yvals);
  
      return { x0, y0, x1, y1, sr };
  }
  
  /**
   * Check whether or not a spatialReference is supported by proj4 library.
   *
   * @param {Object} spatialReference to be checked to see if it's supported by proj4. Can be ESRI SR object or a EPSG string.
   * @param {Function} epsgLookup an optional lookup function for EPSG codes which are not loaded
   * in the proj4 definitions, the function should take a numeric EPSG code and return a Promise
   * resolving with a proj4 style definition string
   * @returns {Object} with the structure {
   *  foundProj: (bool) indicates if the projection was found without a web lookup,
   *  message: (string) provides a reason why the projection was not found,
   *  lookupPromise: (Promise) an promise resolving after any web lookups. Resolves with true or false overall success.
   * }
   */
  function checkProj(spatialReference, epsgLookup) {
      let srcProj;
      let latestProj;
      let epsgKey = true; // indicates we are dealing with an EPSG key
      const res = {
          foundProj: false,
          message: 'Source projection OK',
          lookupPromise: Promise.resolve(true),
      };
  
      const addCode = (idnum) => {
          return `EPSG:${idnum}`;
      };
  
      // determine what our parameter is
      if (spatialReference.wkid) {
          // esri SR with wkid.  also check if it has a latestWkid
          srcProj = addCode(spatialReference.wkid);
          if (spatialReference.latestWkid) {
              latestProj = addCode(spatialReference.latestWkid);
          }
      } else if (spatialReference.wkt) {
          // esri SR with wkt. it is good to go.
          res.foundProj = true;
          epsgKey = false;
      } else if (typeof spatialReference === 'string') {
          srcProj = spatialReference;
      } else if (typeof spatialReference === 'number') {
          srcProj = addCode(String(spatialReference));
      } else {
          // dont know what we got.
          res.message = 'No WKT, WKID, or EPSG code specified on input';
          res.lookupPromise = Promise.resolve(false);
          epsgKey = false;
      }
  
      if (epsgKey) {
          // dealing with an epsg key. check for a definition
  
          // worker function. if we had to get latest wkid from internet,
          // need to also map that result to the normal wkid. but only
          // if the two wkids are different.
          const applyLatest = (latestDef, normalDef) => {
              if (latestDef !== normalDef) {
                  proj4.defs(normalDef, proj4.defs(latestDef));
              }
          };
  
          if (proj4.defs(srcProj)) {
              // already defined in proj4. good.
              res.foundProj = true;
          } else {
              // we currently don't have this in proj4
              if (latestProj && proj4.defs(latestProj)) {
                  // we have the latestWkid projection defined.
                  applyLatest(latestProj, srcProj);
                  res.foundProj = true;
              } else {
                  // need to find a definition
  
                  if (epsgLookup) {
                      res.message = 'Attempting to lookup WKID';
  
                      // function to execute a lookup & store result if success
                      const doLookup = (epsgStr) => {
                          return epsgLookup(epsgStr).then((def) => {
                              if (def === null) {
                                  return false;
                              }
                              proj4.defs(epsgStr, def);
                              return true;
                          });
                      };
  
                      // check the latestWkid first, if it exists (as that wkid is usally the EPSG friendly one)
                      // otherwise make a dummy promise that will just cause the standard wkid promise to run.
                      const latestLookup = latestProj ? doLookup(latestProj) : Promise.resolve(false);
  
                      res.lookupPromise = latestLookup.then((latestSuccess) => {
                          if (latestSuccess) {
                              // found the latestWkid code
                              applyLatest(latestProj, srcProj);
                              return true;
                          } else {
                              // no luck with latestWkid, so lookup on normal code
                              return doLookup(srcProj);
                          }
                      });
                  } else {
                      // no lookup function. no projections for you.
                      res.lookupPromise = Promise.resolve(false);
                      res.message = 'Source projection not recognized by proj4 library';
                  }
              }
          }
      }
  
      return res;
  }
  
  function projectEsriExtentBuilder(esriBundle) {
      return (extent, sr) => {
          const p = localProjectExtent(extent, sr);
          return new esriBundle.Extent(p.x0, p.y0, p.x1, p.y1, p.sr);
      };
  }
  
  function esriServiceBuilder(esriBundle) {
      /**
       * Reproject an esri geometry object on the server. Requires network traffic
       * to esri's Geometry Service, but may be slower than proj4 conversion.
       * Internally it tests 1 point and reprojects it to another spatial reference.
       *
       * @param {url} url for the ESRI Geometry Service
       * @param {geometries} geometries to be projected
       * @param {sr} sr is the target spatial reference
       * @returns {Promise} promise to return reprojected geometries
       */
      return (url, geometries, sr) => {
          return new Promise((resolve, reject) => {
              const params = new esriBundle.ProjectParameters();
  
              // connect to esri server
              const gsvc = new esriBundle.GeometryService(url);
  
              params.geometries = geometries;
              params.outSR = sr;
  
              // call project function from esri server to do conversion
              gsvc.project(
                  params,
                  (projectedExtents) => {
                      resolve(projectedExtents);
                  },
                  (error) => {
                      reject(error);
                  }
              );
          });
      };
  }
  
  /**
   * Checks if two spatial reference objects are equivalent.  Handles both wkid and wkt definitions.
   *
   * @method isSpatialRefEqual
   * @static
   * @param {type} sr1 Esri Spatial Reference First to compare
   * @param {type} sr2 Esri Spatial Reference Second to compare
   * @return {Boolean} true if the two spatial references are equivalent.  False otherwise.
   */
  function isSpatialRefEqual(sr1, sr2) {
      if (sr1.wkid && sr2.wkid) {
          // both SRs have wkids
          return sr1.wkid === sr2.wkid;
      } else if (sr1.wkt && sr2.wkt) {
          // both SRs have wkt's
          return sr1.wkt === sr2.wkt;
      } else {
          // not enough info provided or mismatch between wkid and wkt.
          return false;
      }
  }
  
  module.exports = function (esriBundle) {
      // TODO: Move Point and SpatialReference to its own (geometry) module
      // TODO some of the hardcoded 102### projections might be removed after https://github.com/fgpv-vpgf/fgpv-vpgf/issues/2234
      // TODO consider moving this elsewhere.  state is bad, but these are common, and we have no service for esri defs
      proj4.defs(
          'EPSG:3978',
          '+proj=lcc +lat_1=49 +lat_2=77 +lat_0=49 ' +
              '+lon_0=-95 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'
      );
      proj4.defs(
          'EPSG:3979',
          '+proj=lcc +lat_1=49 +lat_2=77 +lat_0=49 +lon_0=-95 ' +
              '+x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'
      );
      proj4.defs('EPSG:54004', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 ' + '+datum=WGS84 +units=m +no_defs');
      proj4.defs('EPSG:102100', proj4.defs('EPSG:3857'));
      proj4.defs(
          'EPSG:102187',
          '+proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 ' +
              '+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'
      );
      proj4.defs(
          'EPSG:102190',
          '+proj=aea +lat_1=50 +lat_2=58.5 +lat_0=45 +lon_0=-126 ' +
              '+x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'
      );
  
      // add UTM projections
      let utm = 1;
      while (utm <= 60) {
          let zone = utm < 10 ? `0${utm}` : utm;
          proj4.defs(`EPSG:326${zone}`, `+proj=utm +zone=${utm} +ellps=WGS84 +datum=WGS84 +units=m +no_defs`);
          utm++;
      }
  
      return {
          addProjection: proj4.defs, // straight passthrough at the moment, maybe add arg checking (two args)?
          checkProj,
          getProjection: proj4.defs, // straight passthrough at the moment, maybe add arg checking (one arg)?
          esriServerProject: esriServiceBuilder(esriBundle),
          Graphic: esriBundle.Graphic,
          graphicsUtils: esriBundle.graphicsUtils,
          isSpatialRefEqual,
          localProjectExtent,
          localProjectPoint,
          localProjectGeometry: localProjectGeometryBuilder(esriBundle),
          projectGeojson,
          normalizeProj,
          Point: esriBundle.Point,
          projectEsriExtent: projectEsriExtentBuilder(esriBundle),
          SpatialReference: esriBundle.SpatialReference,
      };
  };
  