'use strict';
define(['angular'], function(angular) {
  var mod = angular.module('mapJsUtils_service', []);

  mod.provider('mapJsUtils', function() {
    this.$get = function (gclayers, gaJsUtils, DataStoreFactory) {


      return {
        localiseData: (obj, map) => {
          let format = new ol.format.GeoJSON();
          let copy = gaJsUtils.getCleanFeature(obj);
          let feature = format.readFeature(copy);
          gaJsUtils.localiseGeom(feature.getGeometry(),map);
          gclayers.clearhighLightFeatures();
          gclayers.addhighLightFeature(feature);
          return copy;
        },


        drawSelectedFeature: (obj) => {
          let format = new ol.format.GeoJSON();
          let copy = gaJsUtils.getCleanFeature(obj);
          let feature = format.readFeature(copy);
          gclayers.getDrawLayer().getSource().addFeature(feature);
        },


        getFtiDataStore: (fti) => {
          if (DataStoreFactory.resources && DataStoreFactory.resources.datastores) {
            let dataStore = DataStoreFactory.resources.datastores.find(ds => ds.name === fti.storeName);
            if (dataStore) {
              return dataStore;
            }
          }
          return null;
        },

        /**
         * Calcule la longueur d'une ligne appartenant à une multiligne sur le plan projeté. La ligne est situé au rang index du tableau de lignes de la multiligne.<br>
         * Si index est null/undefined on additionne la longueur de toutes les parties de la multiligne.<br>
         * Si index est inférieur ou égal à la taille du tableau de lignes de la multiligne alors on renvoie la longueur de la 1ère ligne de la multiligne.
         * @param {ol.geom.MultiLineString} multiLineString feature de la portion observée (tronçon ou branchement)
         * @param index rang de la LineString à évaluer
         * @return {number} longeur de la portion linéaire observée
         */
        getMultiLineStringLength: (multiLineString, index = null) => {
          if (Number.isInteger(index) && multiLineString.getLineStrings().length > 0) {
            if (index >= multiLineString.getLineStrings().length) {
              index = 0;
            }
            return multiLineString.getLineStrings()[index].getLength();
          }
          else {
            let multiLineLength = 0;
            for (const linestring of multiLineString.getLineStrings()) {
              multiLineLength += linestring.getLength();
            }
            return multiLineLength;
          }
        },

        /**
         * Retourne les coordonnées du point situé sur la multiligne à la distance fournie depuis le début de la multiligne.<br>
         * Si index est null/undefined on compare la distance aux longeurs cumulées des parties de la multiligne.<br>
         * Si index est supérieur ou égal à la taille du tableau de lignes de la multiligne alors on renvoie la longueur de la 1ère ligne de la multiligne.
         * @param {ol.geom.MultiLineString} multiLineString feature de la portion observée (tronçon ou branchement)
         * @param {number|null} index rang de la ligne à évaluer dans le tableau de lignes de la multiligne.
         * @param {number} distance distance en mètre depuis le début de la multiligne
         * @return {ol.Coordinate|null} coordonnées de la multiligne à la distance fournie depuis le départ de la multiligne
         */
        getMultiLineStringCoordinatesAt: (multiLineString, index, distance) => {
          if (Number.isInteger(index) && multiLineString.getLineStrings().length > 0) {
            if (index >= multiLineString.getLineStrings()) {
              // index est supérieur ou égal à la taille du tableau de lignes de la multiligne
              // on renvoie la longueur de la 1ère ligne de la multiligne
              index = 0;
            }
            const linestring = multiLineString.getLineStrings()[index];
            const fraction = distance / linestring.getLength();
            return linestring.getCoordinateAt(fraction);
          }
          else {
            let accumulatedDistance = 0;
            for (const linestring of multiLineString.getLineStrings()) {

              const lineLength = linestring.getLength();

              // teste si la distance fournie est inférieure ou égale à la longueur des parties évaluées.
              // si oui, alors le point recherché est inclus dans la linestring en cours d'évaluation
              if ((accumulatedDistance + lineLength) >= distance) {

                // détermine à quelle distance est situé le point depuis le début de la ligne
                const distanceFromPartStart = distance - accumulatedDistance;

                // La fraction est un nombre entre 0 et 1, où 0 est le début de la ligne et 1 est la fin.
                const fraction = distanceFromPartStart / lineLength;
                return linestring.getCoordinateAt(fraction);

              }
              else {
                // ajoute la longueur de la ligne et boucle sur la suivante
                accumulatedDistance += lineLength;
              }
            }
          }
          return null;
        },
        /**
         * Inverse le sens d'une geométrie linéaire (LineString ou MultiLineString).
         * Dans le cas d'une MultiLineString on doit donc également inverser l'ordre des LineString simples incluses.
         * @param {ol.geom.LineString|ol.geom.MultiLineString} linearGeometry géométrie fournie dont on veut inverser le sens
         * @return {ol.geom.LineString|ol.geom.MultiLineString} geométrie inversée, c'est à dire le 1er sommet est devenu le dernier sommet.
         */
        getReversedLinearGeometry: (linearGeometry) => {
          // méthode interne: Inverse le sens d'une geométrie LineString.
          const getReverseLineString = (lineStringGeometry) => {
            const reversedLineString = new ol.geom.LineString();
            const lineStringCoordinates = lineStringGeometry.getCoordinates();
            const reversedLineCoordinates = [];
            for (let j = lineStringCoordinates.length -1; j >=0; j--) {
              reversedLineCoordinates.push(lineStringCoordinates[j]);
            }
            reversedLineString.setCoordinates(reversedLineCoordinates);
            return reversedLineString;
          };

          let reversedLinearGeometry;
          if (linearGeometry.getType() === 'MultiLineString') {
            reversedLinearGeometry = new ol.geom.MultiLineString();
            for (let i = linearGeometry.getLineStrings().length -1; i >= 0; i--) {
              const reversedLineString = getReverseLineString(linearGeometry.getLineStrings()[i]);
              reversedLinearGeometry.appendLineString(reversedLineString);
            }
          }
          else {
            reversedLinearGeometry = getReverseLineString(linearGeometry);
          }
          return reversedLinearGeometry;
        },

        /**
         * Calcule la distance entre le point de départ d'une ligne et un point situé sur cette ligne.
         * Il convient de vérifier auparavant si le point est réellement situé sur la ligne
         * @param {number[]} coords coordonnées du point situé sur la ligne
         * @param {ol.geom.MultiLineString | ol.geom.LineString} line géométrie de la ligne
         */
        getDistanceFromLineStart: (coords, line) => {
          const point = new ol.geom.Point(coords);

          // sous-méthode pour une ligne de type ol.geom.LineString uniquement
          const getDistanceFromStart = (lineString) => {
            const lineCoordinates = lineString.getCoordinates();
            let distance = 0;

            for (let i = 1; i < lineCoordinates.length; i++) {

              if (coords[0] === lineCoordinates[i][0] && coords[1] === lineCoordinates[i][1]) {
                // si le point fourni est le sommet du segment de la ligne on retourne la distance existante
                return distance;
              } else {
                // créé une ligne par segment de linestring
                const segment = new ol.geom.LineString([lineCoordinates[i-1], lineCoordinates[i]]);

                if (segment.intersectsExtent(point.getExtent())) {

                  // si le point est inclus dans le segment, on calcule la distance entre le début du segment et le point
                  const segmentPart = new ol.geom.LineString([segment.getCoordinates()[0], coords]);
                  distance += segmentPart.getLength();
                  return distance;
                } else {

                  // si le point n'appartient pas au segment, on ajoute la longueur du segment et on boucle sur le segment suivant
                  distance += segment.getLength();
                }
              }
            }
            return -1;
          };

          let distanceFromLineStart = 0;
          if (line.getType() === 'MultiLineString') {
            for (const lineString of line.getLineStrings()) {
              if (lineString.intersectsExtent(point.getExtent())) {
                distanceFromLineStart = getDistanceFromStart(lineString);
              }
            }
          } else {
            distanceFromLineStart = getDistanceFromStart(line);
          }
          return distanceFromLineStart;
        },
        /**
         * Calcule le gisement d'une LineString openlayers en degrés décimaux
         * @param {ol.geom.LineString} linestring ligne dont on veut connaitre le gisement
         * @see {@link https://www.editions-eyrolles.com/Livre/9782212022872/topographie-et-topomet-1 Topographie et Topométrie modernes - Techniques de mesure et de representation, Serge Milles , Jean Lagofun, pages 113-114, Eyrolles, 1999}
         * @return {number} gisement en degrés
         */
        calculateGisement: (linestring) => {
          const firstCoordinate = linestring.getFirstCoordinate(); // point A
          const lastCoordinate = linestring.getLastCoordinate();   // point B
          const deltaX = lastCoordinate[0] - firstCoordinate[0];
          const deltaY = lastCoordinate[1] - firstCoordinate[1];
          const auxiliaryAngle = Math.atan((deltaX / deltaY));
          let gisRad;

          if (deltaX > 0 && deltaY > 0) {
            // B est à l'Est et au Nord de A
            gisRad = auxiliaryAngle;

          }
          else if ((deltaX > 0 && deltaY < 0) || (deltaX < 0 && deltaY < 0)) {
            // B est à l'Est et au Sud de A (avec auxiliaryAngle < 0)
            // ou B est à l'Ouest et au Sud de A (avec auxiliaryAngle > 0)

            gisRad = Math.PI + auxiliaryAngle;

          }
          else if (deltaX < 0 && deltaY > 0) {
            // B est à l'Ouest et au Nord de A (avec auxiliaryAngle > 0)
            gisRad =  (2* Math.PI) + auxiliaryAngle;
          }
          // convertit l'angle en degrés décimaux
          return gisRad * 180 / Math.PI;
        },

        /**
         * Calcul d'un point par coordonnées polaires (gisement + distance).
         * {@link http://cours-fad-public.ensg.eu/pluginfile.php/1343/mod_resource/content/1/Topo3.pdf CALCULS TOPOMETRIQUES}
         * @param {ol.Coordinate} originPoint point de base du calcul du point de destination.
         * @param {number} gisement angle entre le Nord et le segment origin-destination (en degrés décimaux)
         * @param {number} distance distance entre le point origine et le point destination (en mètres)
         * @return {number[]|undefined} coordonnées du point calculé.
         * Retourne undefined dans l'un des cas suivant:<ul><li>
         *   le point fourni n'est pas un tableau de longeur 2</li><li>
         *   le gisement fourni n'est pas un nombre</li><li>
         *   la distance fournie n'est pas un nombre</li></ul>
         */
        calculateCoodinatesByGisementAndDistance: (originPoint, gisement, distance) => {
          let destination;
          const gisRad = (gisement * Math.PI) / 180;
          if (Array.isArray(originPoint) && originPoint.length === 2
              && Number.isFinite(gisement) && Number.isFinite(distance)) {
            const destX = originPoint[0] + (distance * Math.sin(gisRad));
            const destY = originPoint[1] + (distance * Math.cos(gisRad));
            destination = [destX, destY];
          }
          return destination;
        },

        /**
         * Vérifie si la couche ayant le nom défini en paramètre est présent dans la map.
         * @param {ol.Map} map carte openlayers de l'application MAP
         * @param {string} layerName propriété 'name' de la couche openlayers dont on évalue la présence
         * @return {boolean} true si la couche portant le nom <code>layer>Name</code> est présente dans la map
         */
        mapHasLayerByName: (map, layerName) => {
          return map.getLayers().getArray().some(layer => layer.get('name') === layerName);
        },

        /**
         * Vérifie si la couche ayant la propriété fournie en paramètre est présente dans la map.
         * Si aucune propriété n'est fourni on utilise l'id de la couche pour évaluer sa présence
         * @param {ol.Map} map carte openlayers de l'application MAP
         * @param {string} layerPropertyName nom de la propriété de la couche openlayers dont on évalue la présence
         * @param {string} layerPropertyValue valeur de la propriété de la couche openlayers dont on évalue la présence
         * @return {boolean} true si la couche est présente dans la map
         */
        mapHasLayerByProperty: (map, layerPropertyName, layerPropertyValue) => {
          if (!layerPropertyName) {
            return map.getLayers().getArray().some(layer => layer.getId() === layerPropertyValue);
          }
          return map.getLayers().getArray().some(layer => layer.get(layerPropertyName) === layerPropertyValue);
        },

        /**
         * Récupère une layer de la collection de layers de la map
         * Si aucune propriété n'est fourni on utilise l'id de la couche pour la récupérer
         * @param {ol.Map} map carte openlayers de l'application MAP
         * @param {string} layerPropertyName nom de la propriété de la couche openlayers à récupérer
         * @param {string} layerPropertyValue valeur de la propriété de la couche openlayers à récupérer
         * @return {ol.layer.Vector | null} layer openlayers
         */
        getLayerByProperty: (map, layerPropertyName, layerPropertyValue) => {
          let layerIndex;
          if (!layerPropertyName) {
            layerIndex = map.getLayers().getArray().findIndex(layer => layer.getId() === layerPropertyValue);
          }
          else {
            layerIndex = map.getLayers().getArray().findIndex(layer => layer.get(layerPropertyName) === layerPropertyValue);
          }
          if (layerIndex > -1) {
            return map.getLayers().item(layerIndex);
          }
          else {
            const errorStartSentence = 'mapUtils getLayerByProperty - aucune couche retrouvée dans la '
                + 'map dont ';
            if (layerPropertyName) {
              console.info(errorStartSentence + 'la propriété ' + layerPropertyName + ' = ' + layerPropertyValue);
            }
            else {
              console.info(errorStartSentence + ' l\'id = ' + layerPropertyValue);
            }
          }
          return null;
        },

        /**
         * Récupère ou créé si absente une couche possédant
         * une propriété identifiante
         * <code>layerPropertyName</code> égale à
         * <code>layerPropertyValue</code>.<br>
         * Si la layer est créée, la méthode insère la layer
         * dans la map avant de la retourner.
         * Attention, il ne doit exister qu'une seule layer possédant cette
         * valeur pour la propriété, sinon la layer retrournée sera
         * la première layer rencontrée possédant cette valeur de propriété
         * @param {ol.Map} map carte openlayers de l'application MAP
         * @param {string} layerPropertyName nom de la propriété de la couche
         *                    openlayers à récupérer/créer
         * @param {string} layerPropertyValue valeur de la propriété de
         *                    la couche openlayers à récupérer/créer
         * @return {ol.layer.Vector | null} layer openlayers
         */
        addOrGetLayerByProperty: (map, layerPropertyName, layerPropertyValue) => {
          let mapLayer;
          if (!map.getLayers().getArray().some(
            layer => layer.get(layerPropertyName) === layerPropertyValue)) {
            mapLayer = new ol.layer.Vector();
            mapLayer.set(layerPropertyName, layerPropertyValue);
            map.addLayer(mapLayer);
          }
          return this.$get().getLayerByProperty(map, layerPropertyName, layerPropertyValue);
        },

        /**
         * Récupère une layer G2C de la collection de layers de la map
         * @param {ol.Map} map carte openlayers de l'application MAP
         * @param {string} layerPropertyName nom de la propriété de
         *                    la couche openlayers à récupérer
         * @param {string} layerPropertyValue valeur de la propriété de
         *                    la couche openlayers à récupérer
         * @return {ol.layer.Vector | null} layer openlayers
         */
        getG2cLayerByProperty: (map, layerPropertyName, layerPropertyValue) => {
          let layerIndex;
          if (!layerPropertyName) {
            layerIndex = map.getLayers().getArray().findIndex(
              layer => layer.getId() === layerPropertyValue && layer.get('gctype') === 'g2c');
          }
          else {
            layerIndex = map.getLayers().getArray().findIndex(
              layer => layer.get(layerPropertyName) === layerPropertyValue && layer.get('gctype') === 'g2c');
          }
          if (layerIndex > -1) {
            return map.getLayers().item(layerIndex);
          }
          else {
            const errorStartSentence = 'mapUtils getG2cLayerByProperty - aucune couche retrouvée dans la '
                + 'map dont ';
            if (layerPropertyName) {
              console.info(errorStartSentence + 'la propriété ' + layerPropertyName + ' = ' + layerPropertyValue);
            }
            else {
              console.info(errorStartSentence + ' l\'id = ' + layerPropertyValue);
            }
          }
          return this.$get().getLayerByProperty(map, layerPropertyName, layerPropertyValue);
        },

        /**
         * Convertit une couleur rgb en tableau de couleur ol.Color (ex. rgb(0,0,0) => [0,0,0]).
         * Peut contenir une valeur de transparence
         * @param rgba couleur rouge/vert/bleu/transparence
         */
        rgbToOlColor: (rgba) => {
          let color = [];
          if (rgba !== null && rgba !== undefined && rgba.includes(',')) {

            // on récupère le contenu à l'intérieur des parenthèses d'une chaîne "rgb(x,x,x)"
            const openParenthesisIndex = rgba.indexOf('(');
            const closeParenthesisIndex = rgba.indexOf(')');
            if (openParenthesisIndex > -1 && closeParenthesisIndex > -1) {
              const rgbaBody = rgba.slice(openParenthesisIndex + 1, closeParenthesisIndex);

              const dimensions = rgbaBody.split(',');
              if (dimensions.length === 3) {

                // couleur en rgb
                for (const dim of dimensions) {
                  try {
                    const numDim = Number.parseInt(dim);
                    color.push(numDim);
                  }
                  catch(e) {
                    console.error('rgbToOlColor - Impossible de parser la couleur ', rgba);
                  }
                }
              }
              else if (dimensions.length === 4) {

                // couleur en rgba
                for (let i = 0; i < dimensions.length; i++) {
                  try {
                    if (i < 3) {
                      const numDim = Number.parseInt(dimensions[i]);
                      color.push(numDim);
                    }
                    else {
                      const numAlpha = Number.parseFloat(dimensions[i]);
                      color.push(numAlpha);
                    }
                  }
                  catch(e) {
                    console.error('rgbToOlColor - Impossible de parser la couleur ', rgba);
                  }
                }
              }
              else {
                console.error('rgbToOlColor - Impossible de parser la couleur rgb(a) car elle ne '
                    + 'contient pas 3 ou 4 valeurs ', rgba);
              }
            }
            else {
              console.error('rgbToOlColor - Impossible de parser la couleur car elle ne contient '
                  + 'pas de parenthèses ', rgba);
            }
          }
          else {
            console.error('rgbToOlColor - Impossible de parser la couleur car elle ne contient '
                + 'pas le séparateur virgule ', rgba);
          }
          if (color.length === 0) {
            color.push(...[0,0,0]);
          }
          return color;
        },
        /**
         * Supprime les propriétés d'un ol.Feature (feature.getProperties)
         * Dans OpenLayers 4.6.5, setProperties ne supprime pas une propriété existante devenue null.
         * @see https://github.com/openlayers/openlayers/blob/v4.6.5/src/ol/object.js#L170
         * @param olFeature objet openlayers à purger
         */
        clearFeatureProperties: (olFeature) => {
          const properties = olFeature.getProperties();
          for (const prop in properties) {
            if (properties.hasOwnProperty(prop)) {
              olFeature.unset(prop);
            }
          }
        },

        /**
         * Vérifie si une coordonnée fournie fait partie des coordonnées de la géométrie fournie.
         * Cette logique est plus rapide que de vérifier si le point le plus proche de la géométrie (getClosesPoint) est égal aux coordonnées en paramètre
         * @param {ol.geom.MultiLineString|ol.geom.LineString|ol.geom.MultiPoint|ol.geom.Point|ol.geom.Polygon|ol.geom.MultiPolygon} geometry
         * @param {ol.Coordinate} coordinate
         * @return {boolean} true si la géométrie fournie possède la coordonnée fournie en paramètre
         */
        geometryContainsCoordinate: (geometry, coordinate) => {
          const geometryType = geometry.getType();
          switch (geometryType) {
            case 'Point':
              return geometry.getCoordinates().every((value, index) => value === coordinate[index]);
            case 'MultiPoint':
            case 'LineString':
            case 'Polygon':
              return geometry.getCoordinates().some(coord => coord.every((value, index) => value === coordinate[index]));
            case 'MultiLineString':
            case 'MultiPolygon':
              return geometry.getCoordinates().some(subGeomCoords =>
                  subGeomCoords.some(coord => coord.every((value, index) => value === coordinate[index]))
              );
            default:
              console.log('Type de géométrie non pris en charge.');
              return false;
          }
        }
      };
    };



    this.$get.$inject = ['gclayers', 'gaJsUtils', 'DataStoreFactory'];
  });

  return mod;
});
