text: /* global mapillary:false */
  import { dispatch as d3_dispatch } from 'd3-dispatch';
  import { select as d3_select } from 'd3-selection';
  import { Extent, Projection, Tiler, geoScaleToZoom } from '@id-sdk/math';
  import { utilQsString, utilStringQs } from '@id-sdk/util';
  import { VectorTile } from '@mapbox/vector-tile';
  import Protobuf from 'pbf';
  import RBush from 'rbush';
  
  import { utilRebind } from '../util';
  
  const accessToken = 'MLY|3376030635833192|f13ab0bdf6b2f7b99e0d8bd5868e1d88';
  const apiUrl = 'https://graph.mapillary.com/';
  const baseTileUrl = 'https://tiles.mapillary.com/maps/vtp';
  const mapFeatureTileUrl = `${baseTileUrl}/mly_map_feature_point/2/{z}/{x}/{y}?access_token=${accessToken}`;
  const tileUrl = `${baseTileUrl}/mly1_public/2/{z}/{x}/{y}?access_token=${accessToken}`;
  const trafficSignTileUrl = `${baseTileUrl}/mly_map_feature_traffic_sign/2/{z}/{x}/{y}?access_token=${accessToken}`;
  
  const viewercss = 'mapillary-js/mapillary.css';
  const viewerjs = 'mapillary-js/mapillary.js';
  const minZoom = 14;
  const dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged');
  const tiler = new Tiler().skipNullIsland(true);
  
  let _loadViewerPromise;
  let _mlyActiveImage;
  let _mlyCache;
  let _mlyFallback = false;
  let _mlyHighlightedDetection;
  let _mlyShowFeatureDetections = false;
  let _mlyShowSignDetections = false;
  let _mlyViewer;
  let _mlyViewerFilter = ['all'];
  
  
  // Load all data for the specified type from Mapillary vector tiles
  function loadTiles(which, url, maxZoom, projection) {
      // determine the needed tiles to cover the view
      const proj = new Projection().transform(projection.transform()).dimensions(projection.clipExtent());
      const tiles = tiler.zoomRange(minZoom, maxZoom).getTiles(proj).tiles;
  
      tiles.forEach(function(tile) {
          loadTile(which, url, tile);
      });
  }
  
  
  // Load all data for the specified type from one vector tile
  function loadTile(which, url, tile) {
      const cache = _mlyCache.requests;
      const tileId = `${tile.id}-${which}`;
      if (cache.loaded[tileId] || cache.inflight[tileId]) return;
      const controller = new AbortController();
      cache.inflight[tileId] = controller;
      const requestUrl = url
          .replace('{x}', tile.xyz[0])
          .replace('{y}', tile.xyz[1])
          .replace('{z}', tile.xyz[2]);
  
      fetch(requestUrl, { signal: controller.signal })
          .then(function(response) {
              if (!response.ok) {
                  throw new Error(response.status + ' ' + response.statusText);
              }
              cache.loaded[tileId] = true;
              delete cache.inflight[tileId];
              return response.arrayBuffer();
          })
          .then(function(data) {
              if (!data) {
                  throw new Error('No Data');
              }
              loadTileDataToCache(data, tile, which);
  
              if (which === 'images') {
                  dispatch.call('loadedImages');
              } else if (which === 'signs') {
                  dispatch.call('loadedSigns');
              } else if (which === 'points') {
                  dispatch.call('loadedMapFeatures');
              }
          })
          .catch(function() {
              cache.loaded[tileId] = true;
              delete cache.inflight[tileId];
          });
  }
  
  
  // Load the data from the vector tile into cache
  function loadTileDataToCache(data, tile, which) {
      const vectorTile = new VectorTile(new Protobuf(data));
      let features,
          cache,
          layer,
          i,
          feature,
          loc,
          d;
  
      if (vectorTile.layers.hasOwnProperty('image')) {
          features = [];
          cache = _mlyCache.images;
          layer = vectorTile.layers.image;
  
          for (i = 0; i < layer.length; i++) {
              feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
              loc = feature.geometry.coordinates;
              d = {
                  loc: loc,
                  captured_at: feature.properties.captured_at,
                  ca: feature.properties.compass_angle,
                  id: feature.properties.id,
                  is_pano: feature.properties.is_pano,
                  sequence_id: feature.properties.sequence_id,
              };
              cache.forImageId[d.id] = d;
              features.push({
                  minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
              });
          }
          if (cache.rtree) {
              cache.rtree.load(features);
          }
      }
  
      if (vectorTile.layers.hasOwnProperty('sequence')) {
          features = [];
          cache = _mlyCache.sequences;
          layer = vectorTile.layers.sequence;
  
          for (i = 0; i < layer.length; i++) {
              feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
              if (cache.lineString[feature.properties.id]) {
                  cache.lineString[feature.properties.id].push(feature);
              } else {
                  cache.lineString[feature.properties.id] = [feature];
              }
          }
      }
  
      if (vectorTile.layers.hasOwnProperty('point')) {
          features = [];
          cache = _mlyCache[which];
          layer = vectorTile.layers.point;
  
          for (i = 0; i < layer.length; i++) {
              feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
              loc = feature.geometry.coordinates;
  
              d = {
                  loc: loc,
                  id: feature.properties.id,
                  first_seen_at: feature.properties.first_seen_at,
                  last_seen_at: feature.properties.last_seen_at,
                  value: feature.properties.value
              };
              features.push({
                  minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
              });
          }
          if (cache.rtree) {
              cache.rtree.load(features);
          }
      }
  
      if (vectorTile.layers.hasOwnProperty('traffic_sign')) {
          features = [];
          cache = _mlyCache[which];
          layer = vectorTile.layers.traffic_sign;
  
          for (i = 0; i < layer.length; i++) {
              feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
              loc = feature.geometry.coordinates;
  
              d = {
                  loc: loc,
                  id: feature.properties.id,
                  first_seen_at: feature.properties.first_seen_at,
                  last_seen_at: feature.properties.last_seen_at,
                  value: feature.properties.value
              };
              features.push({
                  minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
              });
          }
          if (cache.rtree) {
              cache.rtree.load(features);
          }
      }
  }
  
  
  // Get data from the API
  function loadData(url) {
      return fetch(url)
          .then(function(response) {
              if (!response.ok) {
                  throw new Error(response.status + ' ' + response.statusText);
              }
              return response.json();
          })
          .then(function(result) {
              if (!result) {
                  return [];
              }
              return result.data || [];
          });
  }
  
  
  // Partition viewport into higher zoom tiles
  function partitionViewport(projection) {
      const z = geoScaleToZoom(projection.scale());
      const z2 = (Math.ceil(z * 2) / 2) + 2.5;   // round to next 0.5 and add 2.5
      const proj = new Projection().transform(projection.transform()).dimensions(projection.clipExtent());
      const tiles = tiler.zoomRange(z2).getTiles(proj).tiles;
      return tiles.map(tile => tile.wgs84Extent);
  }
  
  
  // Return no more than `limit` results per partition.
  function searchLimited(limit, projection, rtree) {
      limit = limit || 5;
  
      return partitionViewport(projection)
          .reduce(function(result, extent) {
              const found = rtree.search(extent.bbox()).slice(0, limit).map(d => d.data);
              return (found.length ? result.concat(found) : result);
          }, []);
  }
  
  
  export default {
      // Initialize Mapillary
      init: function() {
          if (!_mlyCache) {
              this.reset();
          }
  
          this.event = utilRebind(this, dispatch, 'on');
      },
  
      // Reset cache and state
      reset: function() {
          if (_mlyCache) {
              Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });
          }
  
          _mlyCache = {
              images: { rtree: new RBush(), forImageId: {} },
              image_detections: { forImageId: {} },
              signs: { rtree: new RBush() },
              points: { rtree: new RBush() },
              sequences: { rtree: new RBush(), lineString: {} },
              requests: { loaded: {}, inflight: {} }
          };
  
          _mlyActiveImage = null;
      },
  
      // Get visible images
      images: function(projection) {
          const limit = 5;
          return searchLimited(limit, projection, _mlyCache.images.rtree);
      },
  
      // Get visible traffic signs
      signs: function(projection) {
          const limit = 5;
          return searchLimited(limit, projection, _mlyCache.signs.rtree);
      },
  
      // Get visible map (point) features
      mapFeatures: function(projection) {
          const limit = 5;
          return searchLimited(limit, projection, _mlyCache.points.rtree);
      },
  
      // Get cached image by id
      cachedImage: function(imageId) {
          return _mlyCache.images.forImageId[imageId];
      },
  
      // Get visible sequences
      sequences: function(projection) {
          const viewport = projection.clipExtent();
          const min = [viewport[0][0], viewport[1][1]];
          const max = [viewport[1][0], viewport[0][1]];
          const bbox = new Extent(projection.invert(min), projection.invert(max)).bbox();
          const sequenceIds = {};
          let lineStrings = [];
  
          _mlyCache.images.rtree.search(bbox)
              .forEach(function(d) {
                  if (d.data.sequence_id) {
                      sequenceIds[d.data.sequence_id] = true;
                  }
              });
  
          Object.keys(sequenceIds).forEach(function(sequenceId) {
              if (_mlyCache.sequences.lineString[sequenceId]) {
                  lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);
              }
          });
  
          return lineStrings;
      },
  
  
      // Load images in the visible area
      loadImages: function(projection) {
          loadTiles('images', tileUrl, 14, projection);
      },
  
  
      // Load traffic signs in the visible area
      loadSigns: function(projection) {
          loadTiles('signs', trafficSignTileUrl, 14, projection);
      },
  
  
      // Load map (point) features in the visible area
      loadMapFeatures: function(projection) {
          loadTiles('points', mapFeatureTileUrl, 14, projection);
      },
  
  
      // Return a promise that resolves when the image viewer (Mapillary JS) library has finished loading
      ensureViewerLoaded: function(context) {
          if (_loadViewerPromise) return _loadViewerPromise;
  
          // add mly-wrapper
          const wrap = context.container().select('.photoviewer')
              .selectAll('.mly-wrapper')
              .data([0]);
  
          wrap.enter()
              .append('div')
              .attr('id', 'ideditor-mly')
              .attr('class', 'photo-wrapper mly-wrapper')
              .classed('hide', true);
  
          const that = this;
  
          _loadViewerPromise = new Promise((resolve, reject) => {
              let loadedCount = 0;
              function loaded() {
                  loadedCount += 1;
  
                  // wait until both files are loaded
                  if (loadedCount === 2) resolve();
              }
  
              const head = d3_select('head');
  
              // load mapillary-viewercss
              head.selectAll('#ideditor-mapillary-viewercss')
                  .data([0])
                  .enter()
                  .append('link')
                  .attr('id', 'ideditor-mapillary-viewercss')
                  .attr('rel', 'stylesheet')
                  .attr('crossorigin', 'anonymous')
                  .attr('href', context.asset(viewercss))
                  .on('load.serviceMapillary', loaded)
                  .on('error.serviceMapillary', function() {
                      reject();
                  });
  
              // load mapillary-viewerjs
              head.selectAll('#ideditor-mapillary-viewerjs')
                  .data([0])
                  .enter()
                  .append('script')
                  .attr('id', 'ideditor-mapillary-viewerjs')
                  .attr('crossorigin', 'anonymous')
                  .attr('src', context.asset(viewerjs))
                  .on('load.serviceMapillary', loaded)
                  .on('error.serviceMapillary', function() {
                      reject();
                  });
          })
          .catch(function() {
              _loadViewerPromise = null;
          })
          .then(function() {
              that.initViewer(context);
          });
  
          return _loadViewerPromise;
      },
  
  
      // Load traffic sign image sprites
      loadSignResources: function(context) {
          context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );
          return this;
      },
  
  
      // Load map (point) feature image sprites
      loadObjectResources: function(context) {
          context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );
          return this;
      },
  
  
      // Remove previous detections in image viewer
      resetTags: function() {
          if (_mlyViewer && !_mlyFallback) {
              _mlyViewer.getComponent('tag').removeAll();
          }
      },
  
  
      // Show map feature detections in image viewer
      showFeatureDetections: function(value) {
          _mlyShowFeatureDetections = value;
          if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
              this.resetTags();
          }
      },
  
  
      // Show traffic sign detections in image viewer
      showSignDetections: function(value) {
          _mlyShowSignDetections = value;
          if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
              this.resetTags();
          }
      },
  
  
      // Apply filter to image viewer
      filterViewer: function(context) {
          const showsPano = context.photos().showsPanoramic();
          const showsFlat = context.photos().showsFlat();
          const fromDate = context.photos().fromDate();
          const toDate = context.photos().toDate();
          const filter = ['all'];
  
          if (!showsPano) filter.push([ '!=', 'cameraType', 'spherical' ]);
          if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);
          if (fromDate) {
              filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);
          }
          if (toDate) {
              filter.push(['>=', 'capturedAt', new Date(toDate).getTime()]);
          }
  
          if (_mlyViewer) {
              _mlyViewer.setFilter(filter);
          }
          _mlyViewerFilter = filter;
  
          return filter;
      },
  
  
      // Make the image viewer visible
      showViewer: function(context) {
          const wrap = context.container().select('.photoviewer')
              .classed('hide', false);
  
          const isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size();
  
          if (isHidden && _mlyViewer) {
              wrap
                  .selectAll('.photo-wrapper:not(.mly-wrapper)')
                  .classed('hide', true);
  
              wrap
                  .selectAll('.photo-wrapper.mly-wrapper')
                  .classed('hide', false);
  
              _mlyViewer.resize();
          }
  
          return this;
      },
  
  
      // Hide the image viewer and resets map markers
      hideViewer: function(context) {
          _mlyActiveImage = null;
  
          if (!_mlyFallback && _mlyViewer) {
              _mlyViewer.getComponent('sequence').stop();
          }
  
          const viewer = context.container().select('.photoviewer');
          if (!viewer.empty()) viewer.datum(null);
  
          viewer
              .classed('hide', true)
              .selectAll('.photo-wrapper')
              .classed('hide', true);
  
          this.updateUrlImage(null);
  
          dispatch.call('imageChanged');
          dispatch.call('loadedMapFeatures');
          dispatch.call('loadedSigns');
  
          return this.setStyles(context, null);
      },
  
  
      // Update the URL with current image id
      updateUrlImage: function(imageId) {
          if (!window.mocha) {
              const hash = utilStringQs(window.location.hash);
              if (imageId) {
                  hash.photo = 'mapillary/' + imageId;
              } else {
                  delete hash.photo;
              }
              window.location.replace('#' + utilQsString(hash, true));
          }
      },
  
  
      // Highlight the detection in the viewer that is related to the clicked map feature
      highlightDetection: function(detection) {
          if (detection) {
              _mlyHighlightedDetection = detection.id;
          }
  
          return this;
      },
  
  
      // Initialize image viewer (Mapillar JS)
      initViewer: function(context) {
          const that = this;
          if (!window.mapillary) return;
  
          const opts = {
              accessToken: accessToken,
              component: {
                  cover: false,
                  keyboard: false,
                  tag: true
              },
              container: 'ideditor-mly',
          };
  
          // Disable components requiring WebGL support
          if (!mapillary.isSupported() && mapillary.isFallbackSupported()) {
              _mlyFallback = true;
              opts.component = {
                  cover: false,
                  direction: false,
                  imagePlane: false,
                  keyboard: false,
                  mouse: false,
                  sequence: false,
                  tag: false,
                  image: true,        // fallback
                  navigation: true    // fallback
              };
          }
  
          _mlyViewer = new mapillary.Viewer(opts);
          _mlyViewer.on('image', imageChanged);
          _mlyViewer.on('bearing', bearingChanged);
  
          if (_mlyViewerFilter) {
              _mlyViewer.setFilter(_mlyViewerFilter);
          }
  
          // Register viewer resize handler
          context.ui().photoviewer.on('resize.mapillary', function() {
              if (_mlyViewer) _mlyViewer.resize();
          });
  
          // imageChanged: called after the viewer has changed images and is ready.
          function imageChanged(node) {
              that.resetTags();
              const image = node.image;
              that.setActiveImage(image);
              that.setStyles(context, null);
              const loc = [image.originalLngLat.lng, image.originalLngLat.lat];
              context.map().centerEase(loc);
              that.updateUrlImage(image.id);
  
              if (_mlyShowFeatureDetections || _mlyShowSignDetections) {
                  that.updateDetections(image.id, `${apiUrl}/${image.id}/detections?access_token=${accessToken}&fields=id,image,geometry,value`);
              }
              dispatch.call('imageChanged');
          }
  
  
          // bearingChanged: called when the bearing changes in the image viewer.
          function bearingChanged(e) {
              dispatch.call('bearingChanged', undefined, e);
          }
      },
  
  
      // Move to an image
      selectImage: function(context, imageId) {
          if (_mlyViewer && imageId) {
              _mlyViewer.moveTo(imageId)
                  .catch(function(e) {
                      console.error('mly3', e); // eslint-disable-line no-console
                  });
          }
  
          return this;
      },
  
  
      // Return the currently displayed image
      getActiveImage: function() {
          return _mlyActiveImage;
      },
  
  
      // Return a list of detection objects for the given id
      getDetections: function(id) {
          return loadData(`${apiUrl}/${id}/detections?access_token=${accessToken}&fields=id,value,image`);
      },
  
  
      // Set the currently visible image
      setActiveImage: function(image) {
          if (image) {
              _mlyActiveImage = {
                  ca: image.originalCompassAngle,
                  id: image.id,
                  loc: [image.originalLngLat.lng, image.originalLngLat.lat],
                  is_pano: image.cameraType === 'spherical',
                  sequence_id: image.sequenceId
              };
          } else {
              _mlyActiveImage = null;
          }
      },
  
  
      // Update the currently highlighted sequence and selected bubble.
      setStyles: function(context, hovered) {
          const hoveredImageId = hovered && hovered.id;
          const hoveredSequenceId = hovered && hovered.sequence_id;
          const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;
  
          context.container().selectAll('.layer-mapillary .viewfield-group')
              .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); })
              .classed('hovered', function(d) { return d.id === hoveredImageId; });
  
          context.container().selectAll('.layer-mapillary .sequence')
              .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })
              .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });
  
          return this;
      },
  
  
      // Get detections for the current image and shows them in the image viewer
      updateDetections: function(imageId, url) {
          if (!_mlyViewer || _mlyFallback) return;
          if (!imageId) return;
          const cache = _mlyCache.image_detections;
          if (cache.forImageId[imageId]) {
              showDetections(_mlyCache.image_detections.forImageId[imageId]);
          } else {
              loadData(url)
                  .then(detections => {
                      detections.forEach(function(detection) {
                          if (!cache.forImageId[imageId]) {
                              cache.forImageId[imageId] = [];
                          }
                          cache.forImageId[imageId].push({
                              geometry: detection.geometry,
                              id: detection.id,
                              image_id: imageId,
                              value:detection.value
                          });
                      });
  
                      showDetections(_mlyCache.image_detections.forImageId[imageId] || []);
                  });
          }
  
  
          // Create a tag for each detection and shows it in the image viewer
          function showDetections(detections) {
              const tagComponent = _mlyViewer.getComponent('tag');
              detections.forEach(function(data) {
                  const tag = makeTag(data);
                  if (tag) {
                      tagComponent.add([tag]);
                  }
              });
          }
  
  
          // Create a Mapillary JS tag object
          function makeTag(data) {
              const valueParts = data.value.split('--');
              if (!valueParts.length) return;
  
              let tag;
              let text;
              let color = 0xffffff;
  
              if (_mlyHighlightedDetection === data.id) {
                  color = 0xffff00;
                  text = valueParts[1];
                  if (text === 'flat' || text === 'discrete' || text === 'sign') {
                      text = valueParts[2];
                  }
                  text = text.replace(/-/g, ' ');
                  text = text.charAt(0).toUpperCase() + text.slice(1);
                  _mlyHighlightedDetection = null;
              }
  
              var decodedGeometry = window.atob(data.geometry);
              var uintArray = new Uint8Array(decodedGeometry.length);
              for (var i = 0; i < decodedGeometry.length; i++) {
                  uintArray[i] = decodedGeometry.charCodeAt(i);
              }
              const tile = new VectorTile(new Protobuf(uintArray.buffer));
              const layer = tile.layers['mpy-or'];
  
              const geometries = layer.feature(0).loadGeometry();
  
              const polygon = geometries.map(ring =>
                  ring.map(point =>
                      [point.x / layer.extent, point.y / layer.extent]));
  
              tag = new mapillary.OutlineTag(
                  data.id,
                  new mapillary.PolygonGeometry(polygon[0]),
                  {
                      text: text,
                      textColor: color,
                      lineColor: color,
                      lineWidth: 2,
                      fillColor: color,
                      fillOpacity: 0.3,
                  }
              );
  
              return tag;
          }
      },
  
  
      // Return the current cache
      cache: function() {
          return _mlyCache;
      }
  };
  