import { json as d3_json, xml as d3_xml} from 'd3-fetch';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { pairs as d3_pairs } from 'd3-array';
import RBush from 'rbush';
import { iso1A2Codes } from '@rapideditor/country-coder';
import { t, localizer } from '../core/localizer';
import { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs} from '../util';
import {geoExtent, geoScaleToZoom, geoVecAngle, geoVecEqual} from '../geo';
import pannellumPhotoFrame from './pannellum_photo';
import planePhotoFrame from './plane_photo';
import { services } from './';


const owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?';
const tileZoom = 14;
const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
const dispatch = d3_dispatch('loadedImages', 'viewerChanged');
const directionEnum = Object.freeze({
  forward: Symbol(0),
  backward: Symbol(1)
});

let _planeFrame;
let _pannellumFrame;
let _currentFrame;
let _loadViewerPromise;
let _vegbilderCache;

async function fetchAvailableLayers() {
  const params = {
    service: 'WFS',
    request: 'GetCapabilities',
    version: '2.0.0',
  };

  const urlForRequest = owsEndpoint + utilQsString(params);
  const response = await d3_xml(urlForRequest);
  const regexMatcher = /^vegbilder_1_0:Vegbilder(?<image_type>_360)?_(?<year>\d{4})$/;
  const availableLayers = [];
  for (const node of response.querySelectorAll('FeatureType > Name')) {
    const match = node.textContent?.match(regexMatcher);
    if (match) {
      availableLayers.push({
        name: match[0],
        is_sphere: !!match.groups?.image_type,
        year: parseInt(match.groups?.year, 10)
      });
    }
  }
  return availableLayers;
}

function filterAvailableLayers(photoContex) {
  const fromDateString = photoContex.fromDate();
  const toDateString = photoContex.toDate();
  const fromYear = fromDateString ? new Date(fromDateString).getFullYear() : 2016;
  const toYear = toDateString ? new Date(toDateString).getFullYear() : null;
  const showsFlat = photoContex.showsFlat();
  const showsPano = photoContex.showsPanoramic();
  return Array.from(_vegbilderCache.wfslayers.values()).filter(({layerInfo}) => (
    (layerInfo.year >= fromYear) &&
    (!toYear || (layerInfo.year <= toYear)) &&
    ((!layerInfo.is_sphere && showsFlat) || (layerInfo.is_sphere && showsPano))
  ));
}

function loadWFSLayers(projection, margin, wfslayers) {
  const tiles = tiler.margin(margin).getTiles(projection);
  for (const cache of wfslayers) {
    loadWFSLayer(projection, cache, tiles);
  }
}

function loadWFSLayer(projection, cache, tiles) {
  // abort inflight requests that are no longer needed
  for (const [key, controller] of cache.inflight.entries()) {
    const wanted = tiles.some(tile => key === tile.id);
    if (!wanted) {
      controller.abort();
      cache.inflight.delete(key);
    }
  }

  Promise.all(tiles.map(
          tile => loadTile(cache, cache.layerInfo.name, tile)
          )).then(() => orderSequences(projection, cache));
}

/**
* loadNextTilePage() load data for the next tile page in line.
*/
async function loadTile(cache, typename, tile) {
  const bbox = tile.extent.bbox();
  const tileid = tile.id;
  if ((cache.loaded.get(tileid) === true) || cache.inflight.has(tileid)) return;

  const params = {
    service: 'WFS',
    request: 'GetFeature',
    version: '2.0.0',
    typenames: typename,
    bbox: [bbox.minY, bbox.minX, bbox.maxY, bbox.maxX].join(','),
    outputFormat: 'json'
  };

  const controller = new AbortController();
  cache.inflight.set(tileid, controller);

  const options = {
    method: 'GET',
    signal: controller.signal,
  };

  const urlForRequest = owsEndpoint + utilQsString(params);

  let featureCollection;
  try {
    featureCollection = await d3_json(urlForRequest, options);
  } catch {
    cache.loaded.set(tileid, false);
    return;
  } finally {
    cache.inflight.delete(tileid);
  }

  cache.loaded.set(tileid, true);

  if (featureCollection.features.length === 0) { return; }

  const features = featureCollection.features.map(feature => {
    const loc = feature.geometry.coordinates;
    const key = feature.id;
    const properties = feature.properties;
    const {
      RETNING: ca,
      TIDSPUNKT: captured_at,
      URL: image_path,
      URLPREVIEW : preview_path,
      BILDETYPE: image_type,
      METER: metering,
      FELTKODE: lane_code
    } = properties;
    const lane_number = parseInt(lane_code.match(/^[0-9]+/)[0], 10);
    const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward;
    const data = {
      service: 'photo',
      loc,
      key,
      ca,
      image_path,
      preview_path,
      road_reference: roadReference(properties),
      metering,
      lane_code,
      direction,
      captured_at: new Date(captured_at),
      is_sphere: image_type === '360'
    };

    cache.points.set(key, data);

    return {
      minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data
    };
  });

  _vegbilderCache.rtree.load(features);
  dispatch.call('loadedImages');
}

function orderSequences(projection, cache) {
  const {points} = cache;

  const grouped = Array.from(points.values()).reduce((grouped, image) => {
    const key = image.road_reference;
    if (grouped.has(key)) {
      grouped.get(key).push(image);
    } else {
      grouped.set(key, [image]);
    }
    return grouped;
  }, new Map());

  const imageSequences = Array.from(grouped.values()).reduce((imageSequences, imageGroup) => {
    imageGroup.sort((a, b) => {
      if (a.captured_at.valueOf() > b.captured_at.valueOf()) {
        return 1;
      } else if (a.captured_at.valueOf() < b.captured_at.valueOf()) {
        return -1;
      } else {
        const {direction} = a;
        if (direction === directionEnum.forward) {
          return a.metering - b.metering;
        } else {
          return b.metering - a.metering;
        }
      }
    });
    let imageSequence = [imageGroup[0]];
    let angle = null;
    for (const [lastImage, image] of d3_pairs(imageGroup)) {
      if (lastImage.ca === null) {
        const b = projection(lastImage.loc);
        const a = projection(image.loc);
        if (!geoVecEqual(a, b)) {
          angle = geoVecAngle(a, b);
          angle *= (180 / Math.PI);
          angle -= 90;
          angle = angle >= 0 ? angle : angle + 360;
        }
        lastImage.ca = angle;
      }
      if (
              image.direction === lastImage.direction &&
              image.captured_at.valueOf() - lastImage.captured_at.valueOf() <= 20000
              ) {
        imageSequence.push(image);
      } else {
        imageSequences.push(imageSequence);
        imageSequence = [image];
      }
    }
    imageSequences.push(imageSequence);
    return imageSequences;
  }, []);

  cache.sequences = imageSequences.map(images => {
    const sequence = {
      images,
      key: images[0].key,
      geometry : {
        type : 'LineString',
        coordinates : images.map(image => image.loc)
      }
    };
    for (const image of images) {
      _vegbilderCache.image2sequence_map.set(image.key, sequence);
    }
    return sequence;
  });
}

function roadReference(properties) {
  const {
    FYLKENUMMER: county_number,
    VEGKATEGORI: road_class,
    VEGSTATUS: road_status,
    VEGNUMMER: road_number,
    STREKNING: section,
    DELSTREKNING: subsection,
    HP: parcel,
    KRYSSDEL: junction_part,
    SIDEANLEGGSDEL: services_part,
    ANKERPUNKT: anker_point,
    AAR: year,
  } = properties;

  let reference;

  if (year >= 2020) {
    reference = `${road_class}${road_status}${road_number} S${section}D${subsection}`;
    if (junction_part) {
      reference = `${reference} M${anker_point} KD${junction_part}`;
    } else if (services_part) {
      reference = `${reference} M${anker_point} SD${services_part}`;
    }
  } else {
    reference = `${county_number}${road_class}${road_status}${road_number} HP${parcel}`;
  }

  return reference;
}

function localeTimestamp(date) {
  const options = { day: '2-digit', month: '2-digit', year: 'numeric',
    hour: 'numeric', minute: 'numeric', second: 'numeric' };
  return date.toLocaleString(localizer.localeCode(), options);
}

function partitionViewport(projection) {
  const zoom = geoScaleToZoom(projection.scale());
  const roundZoom = (Math.ceil(zoom * 2) / 2) + 2.5;   // round to next 0.5 and add 2.5
  const tiler = utilTiler().zoomExtent([roundZoom, roundZoom]);

  return tiler.getTiles(projection)
    .map(tile => tile.extent);
}

function searchLimited(limit, projection, rtree) {
  limit ??= 5;

  return partitionViewport(projection)
    .reduce((result, extent) => {
      const found = rtree.search(extent.bbox())
        .slice(0, limit)
        .map(d => d.data);

      return result.concat(found);
    }, []);
}


export default {

  init: function () {
    this.event = utilRebind(this, dispatch, 'on');
  },

  reset: async function () {
    if (_vegbilderCache) {
      for (const layer of _vegbilderCache.wfslayers.values()) {
        for (const controller of layer.inflight.values()) {
          controller.abort();
        }
      }
    }

    _vegbilderCache = {
      wfslayers: new Map(),
      rtree: new RBush(),
      image2sequence_map: new Map()
    };

    const availableLayers = await fetchAvailableLayers();
    const {wfslayers} = _vegbilderCache;

    for (const layerInfo of availableLayers) {
      const cache = {
        layerInfo,
        loaded: new Map(),
        inflight: new Map(),
        points: new Map(),
        sequences: []
      };
      wfslayers.set(layerInfo.name, cache);
    }
  },

  images: function (projection) {
    const limit = 5;
    return searchLimited(limit, projection, _vegbilderCache.rtree);
  },


  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 = geoExtent(projection.invert(min), projection.invert(max)).bbox();
    const seen = new Set();
    const line_strings = [];

    for (const {data} of _vegbilderCache.rtree.search(bbox)) {
      const sequence = _vegbilderCache.image2sequence_map.get(data.key);
      if (!sequence) continue;
      const {key, geometry, images} = sequence;
      if (seen.has(key)) continue;
      seen.add(key);
      const line = {
        type: 'LineString',
        coordinates: geometry.coordinates,
        key,
        images
      };
      line_strings.push(line);
  }
    return line_strings;
  },

  cachedImage: function (key) {
    for (const {points} of _vegbilderCache.wfslayers.values()) {
      if (points.has(key)) return points.get(key);
    }
  },

  getSequenceForImage: function (image) {
    return _vegbilderCache?.image2sequence_map.get(image?.key);
  },

  loadImages: async function (context, margin) {
    if (!_vegbilderCache) {
      await this.reset();
    }
    margin ??= 1;
    const wfslayers = filterAvailableLayers(context.photos());
    loadWFSLayers(context.projection, margin, wfslayers);
  },

  photoFrame: function() {
    return _currentFrame;
  },

  ensureViewerLoaded: function(context) {

    if (_loadViewerPromise) return _loadViewerPromise;

    const step = (stepBy) => () => {
      const viewer = context.container().select('.photoviewer');
      const selected = viewer.empty() ? undefined : viewer.datum();
      if (!selected) return;

      const sequence = this.getSequenceForImage(selected);
      const nextIndex = sequence.images.indexOf(selected) + stepBy;
      const nextImage = sequence.images[nextIndex];

      if (!nextImage) return;

      context.map().centerEase(nextImage.loc);
      this.selectImage(context, nextImage.key, true);
    };

    const wrap = context.container().select('.photoviewer')
      .selectAll('.vegbilder-wrapper')
      .data([0]);

    const wrapEnter = wrap.enter()
      .append('div')
      .attr('class', 'photo-wrapper vegbilder-wrapper')
      .classed('hide', true);

    wrapEnter
      .append('div')
      .attr('class', 'photo-attribution fillD');

    const controlsEnter = wrapEnter
      .append('div')
      .attr('class', 'photo-controls-wrap')
      .append('div')
      .attr('class', 'photo-controls');

    controlsEnter
      .append('button')
      .on('click.back', step(-1))
      .text('◄');

    controlsEnter
      .append('button')
      .on('click.forward', step(1))
      .text('►');

    _loadViewerPromise = Promise.all([
      pannellumPhotoFrame.init(context, wrapEnter),
      planePhotoFrame.init(context, wrapEnter)
    ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {
      _pannellumFrame = pannellumPhotoFrame;
      _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));
      _planeFrame = planePhotoFrame;
      _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));
    });

    return _loadViewerPromise;
  },

  selectImage: function(context, key, keepOrientation) {
    const d = this.cachedImage(key);
    this.updateUrlImage(key);

    const viewer = context.container().select('.photoviewer');
    if (!viewer.empty()) { viewer.datum(d); }

    this.setStyles(context, null, true);

    if (!d) return this;

    const wrap = context.container().select('.photoviewer .vegbilder-wrapper');
    const attribution = wrap.selectAll('.photo-attribution').text('');

    if (d.captured_at) {
      attribution
        .append('span')
        .attr('class', 'captured_at')
        .text(localeTimestamp(d.captured_at));
    }

    attribution
      .append('a')
      .attr('target', '_blank')
      .attr('href', 'https://vegvesen.no')
      .call(t.append('vegbilder.publisher'));

    attribution
      .append('a')
      .attr('target', '_blank')
      .attr('href', `https://vegbilder.atlas.vegvesen.no/?year=${d.captured_at.getFullYear()}&lat=${d.loc[1]}&lng=${d.loc[0]}&view=image&imageId=${d.key}`)
      .call(t.append('vegbilder.view_on'));

    _currentFrame = d.is_sphere? _pannellumFrame : _planeFrame;

    _currentFrame
      .showPhotoFrame(wrap)
      .selectPhoto(d, keepOrientation);

    return this;
  },

  showViewer: function (context) {
    const viewer = context.container().select('.photoviewer');
    const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();

    if (isHidden) {
      for (const service of Object.values(services)) {
        if (service === this) continue;
        if (typeof service.hideViewer === 'function') {
          service.hideViewer(context);
        }
      }
      viewer
        .classed('hide', false)
        .selectAll('.photo-wrapper.vegbilder-wrapper')
        .classed('hide', false);
    }
    return this;
  },

  hideViewer: function(context) {
    this.updateUrlImage(null);

    const viewer = context.container().select('.photoviewer');
    if (!viewer.empty()) viewer.datum(null);

    viewer
        .classed('hide', true)
        .selectAll('.photo-wrapper')
        .classed('hide', true);

    context.container().selectAll('.viewfield-group, .sequence')
        .classed('currentView', false);

    return this.setStyles(context, null, true);
  },


  // Updates the currently highlighted sequence and selected bubble.
  // Reset is only necessary when interacting with the viewport because
  // this implicitly changes the currently selected bubble/sequence
  setStyles: function (context, hovered, reset) {
    if (reset) {  // reset all layers
      context.container().selectAll('.viewfield-group')
        .classed('highlighted', false)
        .classed('hovered', false)
        .classed('currentView', false);

      context.container().selectAll('.sequence')
        .classed('highlighted', false)
        .classed('currentView', false);
    }

    const hoveredImageKey = hovered?.key;
    const hoveredSequence = this.getSequenceForImage(hovered);
    const hoveredSequenceKey = hoveredSequence?.key;
    const hoveredImageKeys = hoveredSequence?.images.map(d => d.key) ?? [];

    const viewer = context.container().select('.photoviewer');
    const selected = viewer.empty() ? undefined : viewer.datum();
    const selectedImageKey = selected?.key;
    const selectedSequence = this.getSequenceForImage(selected);
    const selectedSequenceKey = selectedSequence?.key;
    const selectedImageKeys = selectedSequence?.images.map(d => d.key) ?? [];

    // highlight sibling viewfields on either the selected or the hovered sequences
    const highlightedImageKeys = utilArrayUnion(hoveredImageKeys, selectedImageKeys);

    context.container().selectAll('.layer-vegbilder .viewfield-group')
      .classed('highlighted', d => highlightedImageKeys.indexOf(d.key) !== -1)
      .classed('hovered', d => d.key === hoveredImageKey)
      .classed('currentView', d => d.key === selectedImageKey);

    context.container().selectAll('.layer-vegbilder .sequence')
      .classed('highlighted', d => d.key === hoveredSequenceKey)
      .classed('currentView', d => d.key === selectedSequenceKey);

    // update viewfields if needed
    context.container().selectAll('.layer-vegbilder .viewfield-group .viewfield')
      .attr('d', viewfieldPath);

    function viewfieldPath() {
      const d = this.parentNode.__data__;
      if (d.is_sphere && d.key !== selectedImageKey) {
        return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';
      } else {
        return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';
      }
    }

    return this;
  },

  updateUrlImage: function (key) {
    const hash = utilStringQs(window.location.hash);
    if (key) {
      hash.photo = 'vegbilder/' + key;
    } else {
      delete hash.photo;
    }
    window.history.replaceState(null, '', '#' + utilQsString(hash, true));
  },

  validHere: function(extent) {
    const bbox = Object.values(extent.bbox());
    return iso1A2Codes(bbox).includes('NO');
  },


  cache: function () {
    return _vegbilderCache;
  }

};
