import "./css/Map.css";
import "maplibre-gl/dist/maplibre-gl.css";
import maplibregl from "maplibre-gl";
import MiniComponentButtons from "./MiniComponentButtons";
import MarkerSVG from "./img/marker.svg";

/**
 * @summary Map showing photo location
 * @private
 */
export default class Map {
  /**
   * @param {external:photo-sphere-viewer.Viewer} psv The viewer
   * @param {object} [options] Optional settings (can be any of [MapLibre GL settings](https://maplibre.org/maplibre-gl-js-docs/api/map/#map-parameters))
   */
  constructor(psv, options = {}) {
    this.psv = psv;
    this.container = document.createElement("div");
    this.container.classList.add("psv-map", "gvs-map-small");
    this.psv.parent.appendChild(this.container);

    // Create map
    this._mapContainer = document.createElement("div");
    this._map = new maplibregl.Map({
      container: this._mapContainer,
      style: "https://tile-vect.openstreetmap.fr/styles/basic/style.json",
      center: [0, 0],
      zoom: 0,
      hash: "map",
      ...options,
    });
    this._map.addControl(new maplibregl.NavigationControl(), "top-right");

    // Widgets and markers
    this._miniButtons = new MiniComponentButtons(this);
    this.container.appendChild(this._mapContainer);
    this._picMarker = this._getPictureMarker();

    this._map.on("load", () => {
      this._createPicturesTilesLayer();
      this._listenToViewerEvents();
      this._map.resize();
    });

    // Cache for pictures and sequences thumbnails
    this._picThumbUrl = {};
    this._seqThumbPicId = {};
  }

  /**
   * Change map rendering between small or wide
   *
   * @param {boolean} isWide True to make wide
   */
  setWide(isWide) {
    if (isWide) {
      this.container.classList.remove("gvs-map-small");
    } else {
      this.container.classList.add("gvs-map-small");
    }
    this._map.resize();
  }

  /**
   * Reduce component visibility (shown as a badge button)
   *
   * @private
   */
  _minimize() {
    this.container.classList.add("gvs-map-minimized");
  }

  /**
   * Show component as a classic widget (invert operation of minimize)
   *
   * @private
   */
  _maximize() {
    this.container.classList.remove("gvs-map-minimized");
  }

  /**
   * Create pictures/sequences vector tiles layer
   *
   * @private
   */
  _createPicturesTilesLayer() {
    this._map.addSource("geovisio", {
      type: "vector",
      tiles: [this.psv._myApi.getPicturesTilesUrl()],
      minzoom: 0,
      maxzoom: 14,
    });

    this._map.addLayer({
      id: "sequences",
      type: "line",
      source: "geovisio",
      "source-layer": "sequences",
      ...this._getSequencesLayerStyleProperties(),
    });

    this._map.addLayer({
      id: "pictures",
      type: "circle",
      source: "geovisio",
      "source-layer": "pictures",
      ...this._getPicturesLayerStyleProperties(),
    });

    // Map interaction events (pointer cursor, click)
    this._picPopup = new maplibregl.Popup({
      closeButton: false,
      closeOnClick: false,
      offset: 3,
    });

    this._map.on("mouseenter", "pictures", (e) => {
      this._map.getCanvas().style.cursor = "pointer";
      this._attachPreviewToPictures(e, "pictures");
    });

    this._map.on("mouseleave", "pictures", () => {
      this._map.getCanvas().style.cursor = "";
      this._picPopup.remove();
    });

    this._map.on("click", "pictures", (e) => {
      e.preventDefault();
      this.psv.goToPicture(e.features[0].properties.id);
    });

    this._map.on("mouseenter", "sequences", (e) => {
      if (this._map.getZoom() <= 15) {
        this._map.getCanvas().style.cursor = "pointer";
        if (e.features[0].properties.id) {
          this._attachPreviewToPictures(e, "sequences");
        }
      }
    });

    this._map.on("mouseleave", "sequences", () => {
      this._map.getCanvas().style.cursor = "";
      this._picPopup.remove();
    });

    this._map.on("click", "sequences", (e) => {
      e.preventDefault();
      if (e.features[0].properties.id && this._map.getZoom() <= 15) {
        this._getPictureIdForSequence(e.features[0].properties.id).then(
          (picId) => {
            if (picId) {
              this.psv.goToPicture(picId);
            }
          }
        );
      }
    });

    this._map.on("click", (e) => {
      if (e.defaultPrevented === false) {
        this._picPopup.remove();
      }
    });
  }

  /**
   * MapLibre paint/layout properties for pictures layer
   * This is useful when selected picture changes to allow partial update
   *
   * @returns {object} Paint/layout properties
   * @private
   */
  _getPicturesLayerStyleProperties() {
    return {
      paint: {
        "circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 2, 22, 9],
        "circle-color": "#FF6F00",
        "circle-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1],
        "circle-stroke-color": "#ffffff",
        "circle-stroke-width": [
          "interpolate",
          ["linear"],
          ["zoom"],
          17,
          0,
          20,
          2,
        ],
      },
      layout: {},
    };
  }

  /**
   * MapLibre paint/layout properties for sequences layer
   *
   * @returns {object} Paint/layout properties
   * @private
   */
  _getSequencesLayerStyleProperties() {
    return {
      paint: {
        "line-width": [
          "interpolate",
          ["linear"],
          ["zoom"],
          0,
          0.5,
          10,
          2,
          14,
          4,
          16,
          5,
          22,
          3,
        ],
        "line-color": "#FF6F00",
      },
      layout: {
        "line-cap": "square",
      },
    };
  }

  /**
   * Attach a preview popup to a single picture.
   * This is a mouseenter over pictures event handler
   *
   * @param {object} e Event data
   * @param {string} from Calling layer name
   * @private
   */
  _attachPreviewToPictures(e, from) {
    let f = e.features[0];

    let coordinates =
      from === "pictures" ? f.geometry.coordinates.slice() : e.lngLat;
    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }

    // Display thumbnail
    this._picPopup
      .setLngLat(coordinates)
      .setHTML(`<i class="gvs-map-thumb">Loading...</i>`)
      .addTo(this._map);

    this._picPopup._loading = f.properties.id;

    const p =
      from === "pictures"
        ? this._getPictureThumbURL(f.properties.id)
        : this._getPictureIdForSequence(f.properties.id).then((picId) =>
            this._getPictureThumbURL(picId)
          );

    p.then((thumbUrl) => {
      if (this._picPopup._loading === f.properties.id) {
        delete this._picPopup._loading;

        if (thumbUrl) {
          this._picPopup.setHTML(
            `<img class="gvs-map-thumb" src="${thumbUrl}" alt="Thumbnail of hovered picture" />`
          );
        } else {
          this._picPopup.setHTML(`<i>No thumbnail</i>`);
        }
      }
    });
  }

  /**
   * Get picture URL for a given picture ID
   *
   * @param {string} picId The picture ID
   * @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
   * @private
   */
  _getPictureThumbURL(picId) {
    let res = null;

    if (picId) {
      if (this._picThumbUrl[picId] !== undefined) {
        res =
          typeof this._picThumbUrl[picId] === "string"
            ? Promise.resolve(this._picThumbUrl[picId])
            : this._picThumbUrl[picId];
      } else {
        this._picThumbUrl[picId] = this.psv._myApi
          .getPictureThumbnailUrl(picId)
          .then((url) => {
            if (url) {
              this._picThumbUrl[picId] = url;
              return url;
            } else {
              this._picThumbUrl[picId] = null;
            }
          })
          .catch(() => {
            this._picThumbUrl[picId] = null;
          });
        res = this._picThumbUrl[picId];
      }
    }

    return res;
  }

  /**
   * Get a picture thumbnail URL for a given sequence
   *
   * @param {string} seqId The sequence ID
   * @returns {Promise} Promise resolving on picture thumbnail URL, or null on timeout
   * @private
   */
  _getPictureIdForSequence(seqId) {
    let res;
    if (this._seqThumbPicId[seqId] !== undefined) {
      if (typeof this._seqThumbPicId[seqId] === "string") {
        res = Promise.resolve(this._seqThumbPicId[seqId]);
      } else if (this._seqThumbPicId[seqId] === null) {
        res = Promise.resolve(null);
      } else {
        res = this._seqThumbPicId[seqId];
      }
    } else {
      this._seqThumbPicId[seqId] = this.psv._myApi
        .getPictureThumbnailForSequence(seqId)
        .then((picMeta) => {
          if (picMeta) {
            const thumbAsset = Object.values(picMeta.assets).find((a) =>
              a?.roles?.includes("thumbnail")
            );

            if (thumbAsset) {
              this._seqThumbPicId[seqId] = picMeta.id;
              this._picThumbUrl[picMeta.id] = thumbAsset.href;
              return picMeta.id;
            } else {
              this._seqThumbPicId[seqId] = null;
              return null;
            }
          } else {
            this._seqThumbPicId[seqId] = null;
            return null;
          }
        })
        .catch(() => {
          this._seqThumbPicId[seqId] = null;
          return null;
        });
      res = this._seqThumbPicId[seqId];
    }

    return res;
  }

  /**
   * Create a ready-to-use picture marker
   *
   * @returns {maplibregl.Marker} The generated marker
   * @private
   */
  _getPictureMarker() {
    const img = document.createElement("img");
    img.src = MarkerSVG;
    return new maplibregl.Marker({
      element: img,
    });
  }

  /**
   * Start listening to picture changes in PSV
   *
   * @private
   */
  _listenToViewerEvents() {
    // Switched picture
    this.psv.on("picture-loaded", (e, d) => {
      // Show marker corresponding to selection
      this._picMarker
        .setLngLat([d.lon, d.lat])
        .setRotation(d.x)
        .addTo(this._map);

      // Move map to picture coordinates
      this._map.flyTo({
        center: [d.lon, d.lat],
        zoom: this._map.getZoom() < 15 ? 20 : this._map.getZoom(),
        maxDuration: 2000,
      });
    });

    // Picture view rotated
    this.psv.on("view-rotated", (e, d) => {
      this._picMarker.setRotation(d.x);
    });
  }
}
