import { LMap, LTileLayer, LMarker, LControl, LIcon, LFeatureGroup, LMarkerClusterGroup, LGeoJson } from "@vue-leaflet/vue-leaflet";
import Vue from "vue";

Vue.component("l-map", LMap);
Vue.component("l-tile-layer", LTileLayer);
Vue.component("l-marker", LMarker);
Vue.component("l-control", LControl);

const buildMarkerIcon = (color, number) => {
    const iconUrl =
        number !== undefined
            ? `/maps/marker/${color}/${number}/`
            : `/maps/marker/${color}/`;

    return new Licon({
        iconUrl,
        shadowUrl: `/maps/marker-shadow/`,
        iconSize: [44, 64],
        iconAnchor: [22, 64],
        shadowSize:   [48, 68],
        shadowAnchor: [22, 68],
    });
};

const layerUrl = (tileServerConfig, baseLayerName, gridName) => {
    return `${tileServerConfig.url}/tiles/${baseLayerName}/${gridName || 'webmercator'}/{z}/{x}/{y}.png`;
}

const retinaAwareLayerUrl = (tileServerConfig, baseLayerName, gridName, gridNameHq) => {
    const isRetina = false;  // TODO
    const ln = isRetina ? baseLayerName + '_hq' : baseLayerName;
    const gn = isRetina ? (gridNameHq || "webmercator") : (gridName || "webmercator");
    return layerUrl(tileServerConfig, ln, gn);
}

const mapboxStyle = (tileServerConfig, id) => ({
    url: retinaAwareLayerUrl(tileServerConfig, id, "mapbox_webmercator", "mapbox_webmercator_hq"),
    attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
    // @see: https://github.com/Leaflet/Leaflet/issues/6059#issuecomment-367007392
    mapMaxZoom: 20,
    maxZoom: 22,
    maxNativeZoom: 22,
    detectRetina: true,
    tileSize: false ? 1024 : 512,  // TODO
    zoomOffset: false ? -2 : -1,  // TODO
});

const tileStyles = {
    "osm-mapnik": (tileServerConfig) => ({
        url: layerUrl(tileServerConfig, "osm_mapnik"),
        attribution:
            '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
        maxZoom: 19,
        maxNativeZoom: 19,
    }),
    "stamen-toner": (tileServerConfig) => ({
        url: layerUrl(tileServerConfig, "stamen_toner"),
        attribution:
            'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.',
        ext: "png",
        maxZoom: 20,
        maxNativeZoom: 20,
    }),
    "stamen-terrain": (tileServerConfig) => ({
        url: layerUrl(tileServerConfig, "stamen_terrain"),
        attribution:
            'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.',
        ext: "png",
        maxZoom: 20,
        maxNativeZoom: 20,
    }),
    "stadia-osm-bright": (tileServerConfig) => ({
        url: retinaAwareLayerUrl(tileServerConfig, "stadia_osm_bright"),
        attribution:
            '&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>, &copy; <a href="https://openmaptiles.org/">OpenMapTiles</a> &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors',
        maxZoom: 20,
        maxNativeZoom: 20,
    }),
    "stadia-outdoors": (tileServerConfig) => ({
        url: retinaAwareLayerUrl(tileServerConfig, "stadia_outdoors"),
        attribution:
            '&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>, &copy; <a href="https://openmaptiles.org/">OpenMapTiles</a> &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors',
        maxZoom: 20,
        maxNativeZoom: 20,
    }),
    "mapbox-streets": (tileServerConfig) => mapboxStyle(tileServerConfig, "mapbox_streets"),
    "mapbox-outdoors": (tileServerConfig) => mapboxStyle(tileServerConfig, "mapbox_outdoors"),
    "mapbox-light": (tileServerConfig) => mapboxStyle(tileServerConfig, "mapbox_light"),
    "mapbox-dark": (tileServerConfig) => mapboxStyle(tileServerConfig, "mapbox_dark"),
    "mapbox-satellite": (tileServerConfig) => mapboxStyle(tileServerConfig, "mapbox_satellite"),
    "mapbox-pirate": (tileServerConfig) => mapboxStyle(tileServerConfig, "mapbox_pirate"),
};

const GeoFeatureCollection = Vue.component(
    "GeoFeatureCollection",
    {
      props: {
          tileServerConfig: {
              type: Object,
              default: {},
          },
          geojson: {
              type: Array,
              required: true,
          },
          categoryList: {
              type: Array,
              required: false,
          },
          wrapperClass: {
              type: String,
              default: "",
          },
          tileStyle: {
              type: String,
              default: "osm-mapnik",
          },
          initialZoom: {
              type: Number,
              default: 13,
          },
          displayLegend: {
              type: Boolean,
              default: true,
          },
          displayZoomControl: {
              type: Boolean,
              default: true,
          },
          displayPopups: {
              type: Boolean,
              default: true,
          },
          handleClicks: {
              type: Boolean,
              default: true,
          },
          height: {
              type: String,
              default: "50rem",
          },
      },
      data: function () {
          const tileOpts = tileStyles[this.tileStyle](this.tileServerConfig);

          return {
              // Future map reference
              map: null,
              categories: [],
              entityMap: {},
              collections: [],
              featureGroup: LFeatureGroup(),
              zoom: this.initialZoom,
              currentItem: null,
              // See `mapboxStyle` implementation above.
              mapMaxZoom: tileOpts.mapMaxZoom || tileOpts.maxZoom,
              mapOptions: {
                  dragging: true,  // TODO
                  tap: true,  // TODO
                  zoomSnap: 1,
                  zoomControl: this.displayZoomControl,

              },
              tileLayerOptions: {
                  ...tileOpts
              },
          };
      },
      computed: {
          categoryListExpanded: function () {
              return Object.values(this.categories).reduce((result, category) => {
                  return result || category.expanded;
              }, false);
          },
          categoryCount: function () {
              return Object.keys(this.categories).length;
          },
      },
      mounted() {
          this.map = this.$refs.map.mapObject;
          this.initialize(this.categoryList, this.geojson);

          window.addEventListener("keydown", (event) => {
              if (event.defaultPrevented) {
                  return; // Should do nothing if the default action has been cancelled
              }

              let handled = false;
              let key;

              if (event.key !== undefined) {
                  // Handle the event with KeyboardEvent.key and set handled true.
                  key = event.key;
              } else if (event.keyCode !== undefined) {
                  // Handle the event with KeyboardEvent.keyCode and set handled true.
                  key = event.keyCode;
              }

              if (key === 27 || key === "Esc" || key === "Escape") {
                  handled = true;
                  this.closeItemInfo();
              }

              if (handled) {
                  // Suppress "double action" if event handled
                  event.preventDefault();
              }
          });
      },
      // Unbind event listener on component destroy.
      beforeDestroy() {
          window.removeEventListener("hashchange", this.onHashChange);
      },
      methods: {
          initialize(categories, featureCollections) {
              const allFeatures = [];

              // Preprocess all the features and collections.
              // - prepare link to layer and feature collection
              // - prepare latlng
              // - prepare marker shats
              // - add `muted` to featureCollection to control opacity
              featureCollections.forEach((featureCollection) => {
                  // Muted property to control opacity.
                  featureCollection.properties.muted = false;

                  featureCollection.features.forEach(f => {
                      // Marker stash.
                      f.properties.markers = [];
                      // LatLng info (for points).
                      f.properties.latlng = null;
                      // Layer link (for other types).
                      f.properties.layer = null;
                      // Parent FeatureCollection link./
                      f.properties.collection = featureCollection;

                      // Preload images.
                      if (f.properties.image) {
                          const i = new Image();
                          i.src = f.properties.image;
                      }

                      // Keep reference for easier searches.
                      this.entityMap[f.properties.slug] = f;

                      allFeatures.push(f);
                  })
              });


              if (categories) {
                  // Build category list.
                  this.initCategories(categories, featureCollections);

                  // Bind categories to features.
                  allFeatures.forEach((f) => {
                      if (f.properties.category && this.categories[f.properties.category]) {
                          f.properties.categoryObj = this.categories[f.properties.category];
                      }
                  });

                  // Bind categories to featureCollections.
                  featureCollections.forEach(fc => {
                      if (fc.properties.category && this.categories[fc.properties.category]) {
                          fc.properties.categoryObj = this.categories[fc.properties.category];
                      }
                  });
              }

              this.collections = featureCollections;

              // Draw the map.
              this.initMap(featureCollections);
          },
          /**
          * Traverse list of features a build list of categories
          * with features assigned.
          *
          * @param {GeoJSON features} features
          */
          initCategories(inputCategories, featureCollections) {
              const categories = {};

              inputCategories.forEach((cat, index) => {
                  categories[cat.name] = {
                      expanded: false,
                      name: cat.name,
                      color: cat.color,
                      featureCollections: [],
                      muted: false,
                  };
              });

              featureCollections.forEach((featureCollection) => {
                  if (featureCollection.properties.category) {
                      categories[featureCollection.properties.category].featureCollections.push(featureCollection);
                  }
              });

              this.categories = categories;

              if (this.categories.length === 0) {
                  this.displayLegend = false;
              }
          },
          /**
          * Given a list of feature collection objects, add layers to map.
          * @param {GeoJSON featureCollections} featureCollections
          */
          initMap(featureCollections) {
              featureCollections.forEach((featureCollection) => {
                  const {geoJSONLayer, markersLayer} = this.createFeatureCollectionLayers(featureCollection);

                  geoJSONLayer.addTo(this.featureGroup);
                  markersLayer.addTo(this.featureGroup);

                  // Keep links.
                  featureCollection.properties.geoJSONLayer = geoJSONLayer;
                  featureCollection.properties.markersLayer = markersLayer;

                  this.entityMap[featureCollection.properties.slug] = featureCollection;
              });

              this.map.addLayer(this.featureGroup);

              if (featureCollections.length == 1) {
                  // Pan to single feature, likely a point.
                  this.map.panTo(this.featureGroup.getBounds().getCenter());
              } else {
                  // Fit multiple features in a view.
                  this.map.fitBounds(this.featureGroup.getBounds());
              }

              // Initial check - open item detail if in URL.
              this.onUserNavigation();

              // Listen to URL changes when user triggers browser navigation.
              window.addEventListener("popstate", this.onUserNavigation);

          },
          /**
          * Creates two new layers for a given FeatureCollection object: GeoJSON and markers.
          * @param {Object} featureCollection
          * @returns
          */
          createFeatureCollectionLayers(featureCollection) {
              // Markers are clustered for easier orientation.
              const markers = LMarkerClusterGroup({
                  showCoverageOnHover: false,
                  maxClusterRadius: 48,
              });

              // Get color for feature - either from category or fall back to default.
              const colorForFeature = (feature) => {
                  if (feature.properties.color) {
                      return "#" + feature.properties.color;
                  }
                  const cat = feature.properties.categoryObj;
                  return cat ? "#" + cat.color : "#000000";
              };

              // Get style for given feature.
              const style = (feature) => ({
                  fillColor: colorForFeature(feature),
                  weight: 3,
                  opacity: 0.8,
                  color: colorForFeature(feature),
                  fillOpacity: 0.6,
              });

              const markerIconForCategory = (categoryName, number) =>
                  categoryName && this.categories[categoryName]
                      ? buildMarkerIcon(
                            this.categories[categoryName].color,
                            number
                        )
                      : buildMarkerIcon("000000", number);

              const addMarker = (feature, markerPosLatLng, onClick) => {
                  const tooltipTitle = [feature.properties.title, feature.properties.collectionTitle].filter(i => !!i).join(' - ');

                  // add marker
                  const featureMarker = new LMarker(markerPosLatLng, {
                      icon: feature.properties.color
                          ? buildMarkerIcon(
                                feature.properties.color,
                                feature.properties.index
                            )
                          : markerIconForCategory(
                                feature.properties.category,
                                feature.properties.index
                            ),
                  })
                      .on("click", onClick)
                      .bindTooltip(tooltipTitle, {
                          className: "geo-feature-collection-tooltip",
                          direction: "top",
                          offset: [0, -64],
                      });

                  // Add marker to feature marker list.
                  feature.properties.markers.push(featureMarker);

                  // Add item marker to the cluster.
                  markers.addLayer(featureMarker);

                  return featureMarker;
              };

              /**
              * Process Point/MultiPoint type GeoJSON features.
              * Called for each such feature when building the map.
              */
              const pointToLayer = (feature, latlng) => {
                  const onClick = (evt) => {
                      if (this.handleClicks) {
                          this.zoomToPoint(evt.latlng, feature);
                      }
                  };
                  feature.properties.latlng = latlng;
                  return addMarker(feature, latlng, onClick);
              };

              /**
              * Process Polygon/MultiPolygon/LineString/MultiLineString type GeoJSON features.
              * Called for each such feature when building the map.
              */
              const onEachFeature = (feature, layer) => {
                  const markerPosLatLng = [];

                  const markerForPolyCoords = (coords) => {
                      // Find pole of inaccessibility (not centroid) for the polygon
                      // @see: https://github.com/mapbox/polylabel
                      const markerPos = polylabel(coords, 1);
                      markerPosLatLng.push(L.latLng(markerPos[1], markerPos[0]));
                  };

                  const markerForLineStringCoords = (coords) => {
                      // Find a middle sector of LineString and set position to middle of it
                      const sectorCount = coords.length;
                      const sectorIndex = Math.floor((sectorCount - 1) / 2);

                      markerPosLatLng.push(
                          L.latLng(
                              (coords[sectorIndex][1] +
                                  coords[sectorIndex + 1][1]) /
                                  2,
                              (coords[sectorIndex][0] +
                                  coords[sectorIndex + 1][0]) /
                                  2
                          )
                      );
                  };

                  /**
                  * Supported GeoJSON features: Polygon, MultiPolygon, LineString, MultiLineString.
                  * Point/MultiPoint features are better handled by pointToLayer function.
                  * It's better idea to convert Points to small Polygons (ask marek.forster@pirati.cz for conversion tool) as
                  * bounds and zoom methods are not supported for those.
                  **/
                  if (feature.geometry.type == "Polygon") {
                      markerForPolyCoords(feature.geometry.coordinates);
                      feature.properties.layer = layer;
                  } else if (feature.geometry.type == "MultiPolygon") {
                      feature.geometry.coordinates.forEach(markerForPolyCoords);
                      feature.properties.layer = layer;
                  } else if (feature.geometry.type == "LineString") {
                      markerForLineStringCoords(feature.geometry.coordinates);
                      feature.properties.layer = layer;
                  } else if (feature.geometry.type == "MultiLineString") {
                      feature.geometry.coordinates.forEach(
                          markerForLineStringCoords
                      );
                      feature.properties.layer = layer;
                  } else if (feature.geometry.type == "MultiPoint" || feature.geometry.type == "Point") {
                      // Supported via `pointToLayer`, noop here.
                  } else {
                      console.warn(
                          `GeoJSON feature type unsupported: ${feature.geometry.type}`
                      );
                  }

                  if (markerPosLatLng.length) {
                      const onMarkerClick = (evt) => {
                          if (this.handleClicks) {
                              this.zoomToFeature(feature, true, false);
                          }
                      }

                      markerPosLatLng.forEach((pos) => {
                          addMarker(feature, pos, onMarkerClick);

                      });

                      if (this.handleClicks) {
                          // Bind click event on the layer target item.
                          layer.on({
                              click: (evt) => {
                                  this.zoomToFeature(feature, true, false);
                              }
                          });
                      }
                  }

              };

              return {
                  geoJSONLayer: LGeoJson(featureCollection, {
                      style,
                      onEachFeature,
                      pointToLayer,
                  }),
                  markersLayer: markers,
              };
          },
          /**
          * Stores slug in URL query param.
          *
          * @param {Object} item
          */
          pushToUrl(slug) {
              const url = new URL(window.location);

              if (slug === null) {
                  url.searchParams.delete("item");
                  history.pushState({}, "", url);
              } else {
                  url.searchParams.set("item", slug);
                  history.pushState({}, "", url);
              }
          },
          /**
          * Called when user navigates. Will display detail
          * of corresponding item if such exist.
          *
          * @param {Event} evt
          */
          onUserNavigation(evt) {
              const urlParams = new URLSearchParams(window.location.search);

              // If query is present when starting, locate the item and zoom to it.
              if (urlParams.has("item")) {
                  this.focusOnEntity(urlParams.get("item"), false, true);
              } else if (this.currentItem) {
                  this.closeItemInfo(false);
              }
          },
          /**
          * Hide current item detail, drop it from URL.
          * @param {Boolean} updateUrl whether to push new state to history.
          */
          closeItemInfo(updateUrl = true) {
              if (this.currentItem) {
                  this.currentItem = null;

                  if (updateUrl) {
                      this.pushToUrl(null);
                  }
              }
          },
          /**
          * Expand category, show list of FeatureCollection objects belonging to it
          * @param {String} category
          */
          toggleExpandCategory(category) {
              category.expanded = !category.expanded;
          },
          /**
          * Set muted state for whole feature collection category.
          * @param {String} category
          */
          setMutedCategory(category, muted) {
              category.featureCollections.forEach(featureCollection => this.setMuted(featureCollection, muted));
          },
          /**
          * Set muted state for collection.
          * When featureCollection is muted, it will be less opaque in the list. Used for
          * better orientation when there are lots of features.
          * @param {Object} featureCollection
          */
          setMuted(featureCollection, muted) {
              featureCollection.properties.muted = muted;

              if (featureCollection.properties.categoryObj) {
                  // Update muted flag on category this featureCollection belongs to.
                  // Category is muted when all featureCollections in it are muted.
                  featureCollection.properties.categoryObj.muted = featureCollection
                      .properties
                      .categoryObj
                      .featureCollections
                      .map(featureCollection => featureCollection.properties.muted)
                      .reduce((a, b) => a && b, true);
              }

              if (featureCollection.properties.muted) {
                  if (this.featureGroup.hasLayer(featureCollection.properties.geoJSONLayer)) {
                      this.featureGroup.removeLayer(featureCollection.properties.geoJSONLayer);
                  }
                  if (this.featureGroup.hasLayer(featureCollection.properties.markersLayer)) {
                      this.featureGroup.removeLayer(featureCollection.properties.markersLayer);
                  }
              } else {
                  this.featureGroup.addLayer(featureCollection.properties.geoJSONLayer);
                  this.featureGroup.addLayer(featureCollection.properties.markersLayer);
              }
          },
          /**
          * Toggle solo view for a FeatureCollection object.
          * If collection is in solo mode, all other collections are muted. When solo is toggled off, all collections
          * will get unmuted.
          * @param {Object} featureCollection
          */
          toggleSolo(featureCollection) {
              const wasSolo = this.collections
                  .filter(c => c !== featureCollection)
                  .map(c => c.properties.muted)
                  .reduce((a, b) => a && b, true);

              if (wasSolo) {
                  // Unset solo - unmute all items.
                  this.collections.forEach((c) => this.setMuted(c, false));

              } else {
                  // Set solo.
                  this.collections.forEach((c) => this.setMuted(c, c !== featureCollection));
              }
          },
          /**
          * Zoom to a layer.
          *
          * @param {L.Layer} layer
          * @param {Boolean} zoom whether to zoom to the items
          */
          zoomToLayer(layer, zoom = false) {
              if (zoom) {
                  this.map.flyToBounds(layer.getBounds());
              } else {
                  this.map.panInsideBounds(layer.getBounds());
              }
          },
          /**
          * Zoom to a point.
          *
          * @param {L.Latlng} Latlng
          * @param {Object} feature
          * @param {Boolean} updateUrl whether to push new state to history.
          * @param {Boolean} zoom whether to zoom to the item
          */
          zoomToPoint(latlng, feature, updateUrl = true, zoom = false) {
              if (zoom) {
                  this.map.flyTo(latlng, 18);
              } else {
                  this.map.panTo(latlng);
              }
              this.currentItem = feature.properties;

              if (updateUrl) {
                  this.pushToUrl(this.currentItem.slug);
              }

              // Zooming always unmutes.
              this.setMuted(feature.properties.collection, false);
          },
          /**
          * Zoom to a feature.
          *
          * @param {Object} feature
          * @param {Boolean} updateUrl whether to push new state to history.
          * @param {Boolean} zoom whether to zoom to the items
          */
          zoomToFeature(feature, updateUrl = true, zoom = false) {
              this.zoomToLayer(feature.properties.layer, zoom);
              this.currentItem = feature.properties;

              if (updateUrl) {
                  this.pushToUrl(this.currentItem.slug);
              }

              // Zooming always unmutes.
              this.setMuted(feature.properties.collection, false);
          },
          /**
          * Zoom to a feature collection.
          *
          * @param {Object} featureCollection
          * @param {Boolean} updateUrl whether to push new state to history.
          * @param {Boolean} zoom whether to zoom to the items
          */
          zoomToFeatureCollection(fetureCollection, updateUrl = true, zoom = false) {
              this.zoomToLayer(fetureCollection.properties.geoJSONLayer, zoom);
              this.currentItem = fetureCollection.properties;

              if (updateUrl) {
                  this.pushToUrl(this.currentItem.slug);
              }

              // Zooming always unmutes.
              this.setMuted(fetureCollection, false);
          },
          /**
          * Focus on a entity with corresponding slugified URL.
          *
          * @param {String} slugUrl entity slug
          * @param {Boolean} updateUrl whether to push new state to history.
          * @param {Boolean} zoom whether to zoom to the items
          */
          focusOnEntity(slugUrl, updateUrl = true, zoom = false) {
              const entity = this.entityMap[slugUrl];

              if (entity) {
                  if (entity.properties.geoJSONLayer) {
                      // Whole FeatureCollection.
                      this.zoomToFeatureCollection(entity, zoom);
                  } else if (entity.properties.layer) {
                      // Feature - other than Point type.
                      this.zoomToFeature(entity, updateUrl, zoom);
                  } else if (entity.properties.latlng) {
                      // Feature - Point type.
                      this.zoomToPoint(entity.properties.latlng, entity, updateUrl, zoom);
                  }
              }
          },
          /**
          * Stop event propagation, utility fn.
          * @param {Event} evt
          */
          stopPropagation(evt) {
              evt.stopPropagation();
          },
      },
      template: `
        <template>

        </template>
      `,
    }
);

export { GeoFeatureCollection }