import TileWMS from 'ol/source/TileWMS';
import { View, Map } from 'ol';
import { ScaleLine, Zoom, Attribution, FullScreen } from 'ol/control';
import TileLayer from 'ol/layer/Tile';
import { transform, transformExtent, fromLonLat/* , get as getProjection */ } from 'ol/proj';
import { defaults } from 'ol/interaction';
import XYZ from 'ol/source/XYZ';
import VectorTileLayer from 'ol/layer/VectorTile';
import VectorTileSource from 'ol/source/VectorTile';
import { MVT, GeoJSON } from 'ol/format';
import { boundingExtent/* , getTopLeft, getWidth */ } from 'ol/extent';
import Overlay from 'ol/Overlay';
import {
  Fill, Stroke, Style, Circle //RegularShape, Circle as CircleStyle, Text,Icon
} from 'ol/style';
import { asArray } from 'ol/color';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
//import WMTS from 'ol/source/WMTS';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS';
import WMTSCapabilities from 'ol/format/WMTSCapabilities';

import OverlayPositioning from 'ol/OverlayPositioning';
import Geolocation from 'ol/Geolocation.js';
import Feature from 'ol/Feature.js';
import Point from 'ol/geom/Point.js';

import axios from '@/axios-client.js';
import router from '@/router';
import store from '@/store';
import { retrieveFeatureProperties } from '@/utils';

const parser = new WMTSCapabilities();

let dictLayersToMap = {};
let layersCount = 0;

const geolocationStyle = new Style({
  image: new Circle({
    radius: 6,
    fill: new Fill({
      color: '#3399CC',
    }),
    stroke: new Stroke({
      color: '#fff',
      width: 2,
    }),
  }),
});

const mapService = {
  layers: [],

  mvtLayer: undefined,

  content: {},

  overlay: {},

  map: undefined,

  queryParams: {},

  geolocation: undefined, // for geolocation

  geolocationSource: null, // for geolocation

  positionFeature: null, // for geolocation

  lastPosition: null, // for geolocation

  getMap() {
    return this.map;
  },

  destroyMap() {
    this.map = undefined;
  },

  createMap(el, options) {
    const {
      lat,
      lng,
      mapDefaultViewCenter,
      mapDefaultViewZoom,
      maxZoom,
      zoom,
      zoomControl = true,
      fullScreenControl = false,
      geolocationControl = false,
      interactions = { doubleClickZoom: false, mouseWheelZoom: false, dragPan: true },
      controls = [
        new Attribution({ collapsible: false }),
        new ScaleLine({
          units: 'metric',
        }),
      ],
    } = options;

    if (fullScreenControl) {
      controls.push(new FullScreen({ tipLabel: 'Mode plein écran' }));
    }

    const mapOptions = {
      layers: [],
      target: el,
      controls,
      interactions: defaults(interactions),
      view: new View({
        center: transform([ //* since 0 is considered false, check for number instead of just defined (though boolean will pass through)
          Number(lng) ? lng : mapDefaultViewCenter[1],
          Number(lat) ? lat : mapDefaultViewCenter[0],

        ], 'EPSG:4326', 'EPSG:3857'),
        zoom: Number(mapDefaultViewZoom) ? mapDefaultViewZoom : zoom,
        maxZoom
      }),
    };

    this.map = new Map(mapOptions);

    if (zoomControl) {
      this.map.addControl(new Zoom({ zoomInTipLabel: 'Zoomer', zoomOutTipLabel: 'Dézoomer' }));
    }
    if (geolocationControl) {
      this.initGeolocation();
    }

    this.map.once('rendercomplete', () => {
      this.map.updateSize();
    });
    const container = document.getElementById('popup');
    this.content = document.getElementById('popup-content');
    const closer = document.getElementById('popup-closer');

    this.overlay = new Overlay({
      element: container,
      autoPan: true,
      autoPanAnimation: {
        duration: 500,
      },
    });
    let overlay = this.overlay;
    if (closer) {
      closer.onclick = function () {
        overlay.setPosition(undefined);
        closer.blur();
        return false;
      };
    }
    this.map.addOverlay(this.overlay);

    this.map.on('click', this.onMapClick.bind(this));
    // catch event from sidebarLayer to update layers order (since all maps use it now)
    document.addEventListener('change-layers-order', (event) => {
      // Reverse is done because the first layer in order has to be added in the map in last.
      // Slice is done because reverse() changes the original array, so we make a copy first
      this.updateOrder(event.detail.layers.slice().reverse());
    });
    return this.map;
  },

  addRouterToPopup({ featureId, featureTypeSlug, index }) {

    const getFeaturePosition = async (searchParams) => {
      const response = await axios.get(`${store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${router.history.current.params.slug}/feature/${featureId}/position-in-list/?${searchParams.toString()}`);
      return response.data;
    };

    const goToBrowseFeatureDetail = async () => {
      const currentQuery = { ...this.queryParams };
      if (this.queryParams && this.queryParams.filter === 'feature_type_slug') { // when feature_type is the default filter of the project, 
        currentQuery['feature_type_slug'] = featureTypeSlug; // get its slug for the current feature
      }
      const searchParams = new URLSearchParams(currentQuery); // urlSearchParams allow to get rid of undefined values

      if (!index >= 0) { // with mvt feature, there can't be an index
        index = await getFeaturePosition(searchParams);
      }

      router.push({
        name: 'details-signalement-filtre',
        query: {
          ...Object.fromEntries(searchParams.entries()), // transform search params into object and spread it into query
          offset: index
        }
      });
    };

    function goToFeatureDetail() {
      router.push({
        name: 'details-signalement',
        params: {
          slug_type_signal: featureTypeSlug,
          slug_signal: featureId,
        },
      });
    }

    const goToFeatureTypeDetail = () => {
      router.push({
        name: 'details-type-signalement',
        params: {
          feature_type_slug: featureTypeSlug,
        },
      });
    };

    const isFeatureBrowsing = (router.history.current.name === 'project_detail' || router.history.current.name === 'liste-signalements');
    const featureEl = document.getElementById('goToFeatureDetail');
    if (featureEl) featureEl.onclick = isFeatureBrowsing ? goToBrowseFeatureDetail : goToFeatureDetail;
    const featureTypeEl = document.getElementById('goToFeatureTypeDetail');
    if (featureTypeEl) featureTypeEl.onclick = goToFeatureTypeDetail;
  },

  async onMapClick (event) {
    //* retrieve features under pointer
    const features = this.map.getFeaturesAtPixel(event.pixel, {
      layerFilter: (l) => l === this.mvtLayer || this.olLayer
    });
    //* prepare popup content
    if (features && features.length > 0 && this.content) {
      const featureId = features[0].properties_ ? features[0].properties_.feature_id : features[0].id_;
      const isEdited = router.history.current.name === 'editer-signalement' &&
        router.history.current.params.slug_signal === featureId; //* avoid opening popup on feature currently edited
      if (featureId && !isEdited) {
        const popupContent = await this._createContentPopup(features[0]);
        this.content.innerHTML = popupContent.html;
        this.overlay.setPosition(event.coordinate);
        this.addRouterToPopup({
          featureId,
          featureTypeSlug: popupContent.feature_type ? popupContent.feature_type.slug : '',
          index: popupContent.index,
        });
      }
    } else if (this.layers) { // If no feature under the mouse pointer, attempt to find a query layer
      const queryLayer = this.layers.find(x => x.query);
      if (queryLayer) {
        // pour compatibilité avec le proxy django
        const proxyparams = [
          'request',
          'service',
          'srs',
          'version',
          'bbox',
          'height',
          'width',
          'layers',
          'query_layers',
          'info_format', 'x', 'y', 'i', 'j',
        ];
        const url = this.getFeatureInfoUrl(event, queryLayer);
        const urlInfos = url ? url.split('?') : [];
        const urlParams = new URLSearchParams(urlInfos[1]);
        const params = {};
        Array.from(urlParams.keys()).forEach(param => {
          if (proxyparams.indexOf(param.toLowerCase()) >= 0) {
            params[param.toLowerCase()] = urlParams.get(param);
          }
        });
        params.url = urlInfos[0];
        axios.get(
          window.proxy_url,
          { params }
        ).then(response => {
          const data = response.data;
          const err = typeof data === 'object' ? null : data;
          if (data.features || err) this.showGetFeatureInfo(err, event, data, queryLayer);
        }).catch(error => {
          throw error;
        });
      }
    }

  },

  showGetFeatureInfo: function (err, event, data, layer) {
    let content;
    if (err) {
      content = `
          <h4>${layer.options.title}</h4>
          <p>Données de la couche inaccessibles</p>
      `;
      this.content.innerHTML = content;
      this.overlay.setPosition(event.coordinate);
    } else { // Otherwise show the content in a popup
      const contentLines = [];
      let contentTitle;
      if (data.features.length > 0) {
        Object.entries(data.features[0].properties).forEach(entry => {
          const [key, value] = entry;
          if (key !== 'bbox') {
            contentLines.push(`<div>${key}: ${value}</div>`);
          }
        });
        contentTitle = `<h4>${layer.options.title}</h4>`;
        content = contentTitle.concat(contentLines.join(''));

        this.content.innerHTML = content;
        this.overlay.setPosition(event.coordinate);
      }
    }
  },

  getFeatureInfoUrl(event, layer) {
    const olLayer = dictLayersToMap[layer.id];
    const source = olLayer.getSource();
    const viewResolution = this.map.getView().getResolution();
    let url;
    const wmsOptions = { info_format: 'application/json', query_layers: layer.options.layers };
    if (source && source.getFeatureInfoUrl) {
      url = source.getFeatureInfoUrl(event.coordinate, viewResolution, 'EPSG:3857', wmsOptions);
    }
    return url;
  },

  fitBounds(bounds) {
    let ext = boundingExtent([[bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]]);
    ext = transformExtent(ext, 'EPSG:4326', 'EPSG:3857');
    this.map.getView().fit(ext, { padding: [25, 25, 25, 25], maxZoom: 16 });

  },

  fitExtent(ext) {
    //ext = transformExtent(ext, 'EPSG:4326', 'EPSG:3857');
    this.map.getView().fit(ext, { padding: [25, 25, 25, 25] });

  },

  addLayers: function (layers, serviceMap, optionsMap, schemaType) {
    this.layers = layers;
    if (layers) { //* if admin has defined basemaps for this project
      layersCount = 0;
      layers.forEach((layer) => this.addConfigLayer(layer));
    } else { //* else when no basemaps defined
      optionsMap.noWrap = true;
      if (schemaType === 'wms') {
        this.addWMSLayer(serviceMap, optionsMap);
      } else if (schemaType === 'wmts') {
        this.addWMTSLayerFromCapabilities(serviceMap, optionsMap);
      } else {
        const layer = new TileLayer({
          source: new XYZ({
            attributions: optionsMap.attribution,
            url: serviceMap.replace('{s}', '{a-c}')
          })
        });
        this.map.addLayer(layer);
      }
    }
  },

  addConfigLayer: async function (layer) {
    if (layer) {
      layersCount += 1;
      const options = layer.options;
      if (options) {
        options.noWrap = true;
        options['opacity'] = layer.opacity;
        if (layer.schema_type === 'wms') {
          if (layer.queryable) options['title'] = layer.title; // wasn't effective before, is it still necessary now that title will be added ?
          dictLayersToMap[layer.id] = this.addWMSLayer(layer.service, options);
        } else if (layer.schema_type === 'wmts') {
          const newLayer = await this.addWMTSLayerFromCapabilities(layer.service, options);
          dictLayersToMap[layer.id] = newLayer;
        } else if (layer.schema_type === 'tms') {
          const layerTms = new TileLayer({
            source: new XYZ({
              attributions: options.attribution,
              url: layer.service.replace('{s}', '{a-c}')
            })
          });
          layerTms.setOpacity(parseFloat(options.opacity));
          this.map.addLayer(layerTms);
          dictLayersToMap[layer.id] = layerTms;
        }
      }
      dictLayersToMap[layer.id].setZIndex(layersCount);
    }
  },

  addWMSLayer: function (url, options) {
    options.VERSION = options.version || '1.3.0'; // pour compatibilité avec le proxy django
    const source = new TileWMS({
      attributions: options.attribution,
      url: url,
      crossOrigin: 'anonymous',
      params: options
    });

    const layer = new TileLayer({
      source: source,
      opacity: parseFloat(options.opacity),
    });
    this.map.addLayer(layer);
    return layer;
  },

  getWMTSLayerCapabilities: async function (url) {
    // adapted from : https://openlayers.org/en/latest/examples/wmts-layer-from-capabilities.html
    // get capabilities with request to the service
    try {
      const response = await fetch(url);
      const text = await response.text();
      const capabilities = parser.read(text);
      return capabilities;
    } catch (error) {
      console.error(error);
    }
  },
    
  addWMTSLayerFromCapabilities: async function (url, options) {
    // adapted from : https://git.neogeo.fr/onegeo-suite/sites/onegeo-suite-site-maps-vuejs/-/blob/draft/src/services/MapService.ts
    const wmtsCapabilities = await this.getWMTSLayerCapabilities(url);
    const { layer, opacity, attributions, format, ignoreUrlInCapabiltiesResponse } = options;
    let sourceOptions;
    try {
      if (format) {
        sourceOptions = optionsFromCapabilities(wmtsCapabilities, { layer, format });
      } else {
        sourceOptions = optionsFromCapabilities(wmtsCapabilities, { layer });
      }
    }
    catch (e) {
      console.error(e);
      if (e.message == 'projection is null') {
        return 'Projection non reconnue';
      }
      else {
        return 'Problème d\'analyse du getCapabilities';
      }
    }

    if (ignoreUrlInCapabiltiesResponse) {
      var searchMask = 'request(=|%3D)getCapabilities';
      var regEx = new RegExp(searchMask, 'ig');
      var replaceMask = '';
      sourceOptions.urls[0] = url.replace(regEx, replaceMask);
    }
    sourceOptions.attributions = attributions;
    sourceOptions.crossOrigin= 'anonymous';
    if (layer === 'ORTHOIMAGERY.ORTHOPHOTOS') {
      // un peu bourrin mais il semble y avoir qq chose de spécifique avec cette couche ORTHO
      // https://geoservices.ign.fr/documentation/services/utilisation-web/affichage-wmts/openlayers-et-wmts
      sourceOptions.tileGrid = new WMTSTileGrid({
        origin: [-20037508,20037508],
        resolutions: [
          156543.03392804103,
          78271.5169640205,
          39135.75848201024,
          19567.879241005125,
          9783.939620502562,
          4891.969810251281,
          2445.9849051256406,
          1222.9924525628203,
          611.4962262814101,
          305.74811314070485,
          152.87405657035254,
          76.43702828517625,
          38.218514142588134,
          19.109257071294063,
          9.554628535647034,
          4.777314267823517,
          2.3886571339117584,
          1.1943285669558792,
          0.5971642834779396,
          0.29858214173896974,
          0.14929107086948493,
          0.07464553543474241
        ],
        matrixIds: ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19'],
      });
    }

    const newLayer = new TileLayer({
      opacity: parseFloat(opacity) || 1, 
      source: new WMTS(sourceOptions),
    });

    this.map.addLayer(newLayer);
    return newLayer;
  },

  // Remove the base layers (not the features)
  removeLayers: function () {
    Object.values(dictLayersToMap).forEach(element => {
      this.map.removeLayer(element);
    });
    dictLayersToMap = {};
  },

  updateOpacity(layerId, opacity) {
    const layer = dictLayersToMap[layerId];
    if (layer) {
      layer.setOpacity(parseFloat(opacity));
    } else {
      console.error(`Layer with id: ${layerId} couldn't be found for opacity update`);
    }
  },

  updateOrder(layers) {
    // First remove existing layers undefined
    layers = layers.filter(function (x) {
      return x !== undefined;
    });
    this.removeLayers();
    // Redraw the layers
    this.addLayers(layers);
  },

  retrieveFeatureStyle: function (featureType, properties) {
    const { colors_style, customfield_set } = featureType;
    let { color, opacity } = featureType;
    if (colors_style && colors_style.custom_field_name && customfield_set) {
      const customField = customfield_set.find((el) => el.name === colors_style.custom_field_name);
      if (customField) {
        const fieldType = customField.field_type;
        let currentValue = properties[colors_style.custom_field_name];
        if (currentValue && typeof currentValue === 'string') currentValue = currentValue.trim(); // remove leading and trailing whitespaces
        switch (fieldType) {
        case 'list':
          if (currentValue) {
            color = colors_style.colors && colors_style.colors[currentValue];
            opacity = colors_style.opacities && colors_style.opacities[currentValue];
          }
          break;
        case 'char': //* if the custom field is supposed to be a string
          //* check if its current value is empty or not, to select a color | https://redmine.neogeo.fr/issues/14048
          color = colors_style.value.colors && colors_style.value.colors[currentValue ? 'Non vide' : 'Vide'];
          opacity = colors_style.value.opacities && colors_style.value.opacities[currentValue ? 'Non vide' : 'Vide'];
          break;
        case 'boolean':
          color = colors_style.value.colors && colors_style.value.colors[currentValue ? 'Coché' : 'Décoché'];
          opacity = colors_style.value.opacities && colors_style.value.opacities[currentValue ? 'Coché' : 'Décoché'];
          break;
        }
      }
    }
    return { color, opacity };
  },

  addVectorTileLayer: function ({ url, project_slug, featureTypes, formFilters = {}, queryParams = {} }) {
    const projectId = project_slug.split('-')[0];
    const format_cfg = {/*featureClass: Feature*/ };
    const mvt = new MVT(format_cfg);

    function customLoader(tile, src) {
      tile.setLoader(function(extent, resolution, projection) {
        const token = () => {
          const re = new RegExp('csrftoken=([^;]+)');
          const value = re.exec(document.cookie);
          return (value != null) ? unescape(value[1]) : null;
        };
        fetch(src, {
          credentials: 'include',
          headers: {
            'X-CSRFToken': token()
          },
        }).then(function(response) {
          response.arrayBuffer().then(function(data) {
            const format = tile.getFormat(); // ol/format/MVT configured as source format
            const features = format.readFeatures(data, {
              extent: extent,
              featureProjection: projection
            });
            tile.setFeatures(features);
          });
        });
      });
    }

    const options = {
      urls: [],
      matrixSet: 'EPSG:3857',
      tileLoadFunction: customLoader,
    };
    options.format = mvt;
    const layerSource = new VectorTileSource(options);
    layerSource.setTileUrlFunction((p0) => {
      return `${url}/?tile=${p0[0]}/${p0[1]}/${p0[2]}&project_id=${projectId}`;
    });
    const styleFunction = (feature) => this.getStyle(feature, featureTypes, formFilters);
    this.mvtLayer = new VectorTileLayer({
      style: styleFunction,
      source: layerSource
    });
    this.featureTypes = featureTypes; // store featureTypes for popups
    this.projectSlug = project_slug; // store projectSlug for popups
    this.queryParams = queryParams; // store queryParams for popups

    this.mvtLayer.setZIndex(30);
    this.map.addLayer(this.mvtLayer);
    window.layerMVT = this.mvtLayer;
  },

  /**
   * Determines the style for a given feature based on its type and applicable filters.
   * 
   * @param {Object} feature - The feature to style.
   * @param {Array} featureTypes - An array of available feature types.
   * @param {Object} formFilters - Filters applied through the form.
   * @returns {ol.style.Style} - The OpenLayers style for the feature.
   */
  getStyle: function (feature, featureTypes, formFilters) {
    const properties = feature.getProperties();
    let featureType;

    // Determine the feature type. Differentiate between GeoJSON and MVT sources.
    if (properties && properties.feature_type) {
      // Handle GeoJSON feature type
      featureType = featureTypes
        .find((ft) => ft.slug === (properties.feature_type.slug || properties.feature_type));
    } else {
      // Handle MVT feature type
      featureType = featureTypes.find((x) => x.slug.split('-')[0] === '' + properties.feature_type_id);
    }

    if (featureType) {
      // Retrieve the style (color, opacity) for the feature.
      const { color, opacity } = this.retrieveFeatureStyle(featureType, properties);
      let colorValue = '#000000'; // Default color

      // Determine the color value based on the feature type.
      if (color && color.value && color.value.length) {
        colorValue = color.value;
      } else if (typeof color === 'string' && color.length) {
        colorValue = color;
      }

      // Convert the color value to RGBA and apply the opacity.
      const rgbaColor = asArray(colorValue);
      rgbaColor[3] = opacity || 0.5; // Default opacity

      // Define the default style for the feature.
      const defaultStyle = new Style({
        image: new Circle({
          fill: new Fill({ color: rgbaColor }),
          stroke: new Stroke({ color: colorValue, width: 2 }),
          radius: 5,
        }),
        stroke: new Stroke({ color: colorValue, width: 2 }),
        fill: new Fill({ color: rgbaColor }),
      });

      // Define a hidden style to apply when filters are active.
      const hiddenStyle = new Style();

      // Apply filters based on feature type, status, and title.
      if (formFilters) {
        if (formFilters.type && formFilters.type.length > 0 && !formFilters.type.includes(featureType.slug)) {
          return hiddenStyle;
        }
        if (formFilters.status && formFilters.status.length > 0 && !formFilters.status.includes(properties.status)) {
          return hiddenStyle;
        }
        if (formFilters.title && !properties.title.toLowerCase().includes(formFilters.title.toLowerCase())) {
          return hiddenStyle;
        }
      }

      // Return the default style if no filters are applied or if the feature passes the filters.
      return defaultStyle;
    } else {
      console.error('No corresponding featureType found.');
      return new Style();
    }
  },

  addFeatures: function ({ features, filter = {}, featureTypes, addToMap = true, project_slug, queryParams = {} }) {
    console.log('addToMap', addToMap);
    const drawSource = new VectorSource();
    let retour;
    let index = 0;
    features.forEach((feature) => {
      try {
        if (feature.properties) {
          feature.properties['index'] = index;
          index += 1;
        }
        retour = new GeoJSON().readFeature(feature, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }, featureTypes);
        drawSource.addFeature(retour);
      } catch (err) {
        console.error(err);
      }
    });
    const styleFunction = (feature) => this.getStyle(feature, featureTypes, filter);
    const olLayer = new VectorLayer({
      source: drawSource,
      style: styleFunction,
    });
    olLayer.setZIndex(29);
    this.map.addLayer(olLayer);
    this.olLayer = olLayer;
    this.drawSource = drawSource;
    this.featureTypes = featureTypes; // store featureTypes for popups
    this.projectSlug = project_slug; // store projectSlug for popups
    this.queryParams = queryParams; // store queryParams for popup routes
    return drawSource;
  },

  removeFeatures: function () {
    this.drawSource.clear();
  },

  addMapEventListener: function (eventName, callback) {
    this.map.on(eventName, callback);
  },

  createCustomFiedsContent(featureType, feature) {
    const { customfield_set } = featureType;
    // generate html for each customField configured to be displayed
    let rows = '';
    for (const { label, name } of customfield_set) {
      const value = feature.getProperties()[name];
      // check if the value is not null nor undefined (to allow false value if boolean)
      if (featureType.displayed_fields.includes(name) && value !== null && value !== undefined) {
        rows += `<div class="customField-row">${label} : ${value}</div>`;
      }
    }
    // wrap all rows into customFields container
    return rows.length > 0 ?
      `<div id="customFields">
        <div class="ui divider"></div>
        <h5>Champs personnalisés</h5>
        ${rows}
      </div>` : '';
  },

  _createContentPopup: async function (feature) {
    const properties = await retrieveFeatureProperties(feature, this.featureTypes, this.projectSlug);
    const { feature_type, index, status, updated_on, created_on, creator, display_last_editor } = properties; // index is used to retrieve feature by query when browsing features
    const { displayed_fields } = feature_type;
    // generate html for each native fields
    const statusHtml = `<div>Statut : ${status}</div>`;
    const featureTypeHtml = `<div>Type de signalement : ${feature_type ? '<a id="goToFeatureTypeDetail" class="pointer">' + feature_type.title + '</a>' : 'Type de signalement inconnu'}</div>`;
    const updatedOnHtml = `<div>Dernière mise à jour : ${updated_on}</div>`;
    const createdOnHtml = `<div>Date de création : ${created_on}</div>`;
    const creatorHtml = creator ? `<div>Auteur : ${creator}</div>` : '';
    const lastEditorHtml = display_last_editor ? `<div>Dernier éditeur : ${display_last_editor}</div>` : '';
    // wrapping up finale html to fill popup, filtering native fields to display and adding filtered customFields
    const html = `<h4>
                    <a id="goToFeatureDetail" class="pointer">${feature.getProperties ? feature.getProperties().title : feature.title}</a>
                  </h4>
                  <div class="fields">
                    ${displayed_fields.includes('status') ? statusHtml : ''}
                    ${displayed_fields.includes('feature_type') ? featureTypeHtml : ''}
                    ${displayed_fields.includes('updated_on') ? updatedOnHtml : ''}
                    ${displayed_fields.includes('created_on') ? createdOnHtml : ''}
                    ${displayed_fields.includes('display_creator') ? creatorHtml : ''}
                    ${displayed_fields.includes('display_last_editor') ? lastEditorHtml : ''}
                    ${this.createCustomFiedsContent(feature_type, feature)}
                  </div>`;
    return { html, feature_type, index };
  },

  zoom(zoomlevel) {
    this.map.getView().setZoom(zoomlevel);
  },

  zoomTo(location, zoomlevel, lon, lat) {
    if (lon && lat) {
      location = [+lon, +lat];
    }
    this.map.getView().setCenter(transform(location, 'EPSG:4326', 'EPSG:3857'));
    this.zoom(zoomlevel);
  },

  animateTo(center, zoom) {
    this.map.getView().animate({ center, zoom });
  },

  addOverlay(loc, zoom) {
    const pos = fromLonLat(loc);
    const marker = new Overlay({
      position: pos,
      positioning: OverlayPositioning.CENTER_CENTER,
      element: document.getElementById('marker'),
      stopEvent: false,
    });
    this.map.addOverlay(marker);
    this.animateTo(pos, zoom);
  },

  initGeolocation() {
    this.geolocation = new Geolocation({
      // enableHighAccuracy must be set to true to have the heading value.
      trackingOptions: {
        enableHighAccuracy: true,
      },
      projection: this.map.getView().getProjection(),
    });
    
    // handle this.geolocation error.
    this.geolocation.on('error', (error) =>  {
      console.error(error.message);
    });
    
    this.positionFeature = new Feature();
    this.positionFeature.setStyle( geolocationStyle );
    
    this.geolocation.on('change:position', () => {
      this.lastPosition = this.geolocation.getPosition();
      console.log('change:position', this.lastPosition); // keeping this console.log for debug purpose in case needed
      this.changeTrackerPosition();
    });
    
    this.geolocationSource = new VectorSource({
      features: [this.positionFeature],
    });    
    new VectorLayer({
      map: this.map,
      source: this.geolocationSource,
    });
  },
  
  changeTrackerPosition() {
    console.log('changeTrackerPosition', this.lastPosition); // keeping this console.log for debug purpose in case needed
    if (this.lastPosition) {
      this.positionFeature.setGeometry(new Point(this.lastPosition));
      this.animateTo(this.lastPosition, 16);
    }
  },

  displayGeolocationPoint(isVisible) {
    let features = this.geolocationSource.getFeatures();
    if (!features) return;
    const hiddenStyle = new Style(); // hide the feature
    for (let i = 0; i < features.length; i++) {
      features[i].setStyle(isVisible ? geolocationStyle : hiddenStyle);
    }
  },

  toggleGeolocation(isTracking) {
    if (this.geolocation) {
      this.geolocation.setTracking(isTracking);
      if (this.geolocationSource) {
        this.displayGeolocationPoint(isTracking);
        if (isTracking) {
          this.changeTrackerPosition();
        }
      }
    }
  },

  getMapCenter() {
    const location = this.map.getView().getCenter();
    if (location) {
      return transform(location, 'EPSG:3857', 'EPSG:4326');
    }
    return null;
  }
};

export default mapService;