/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 Dan "Ducky" Little
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/** This is used to define a *vector* layer source,
 *  it does *not* link this to a data source.
 *
 */

import { parseBoolean, transformProperties, joinUrl, requEstimator } from "../../../util";

import GML2Format from "ol/format/GML2";
import GeoJSONFormat from "ol/format/GeoJSON";
import EsriJsonFormat from "ol/format/EsriJSON";
import { tile, bbox } from "ol/loadingstrategy";
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import { createXYZ } from "ol/tilegrid";
import { getEditStyle } from "./edit";
import { EDIT_LAYER_NAME } from "../../../defaults";

// WARNING! This is a monkey patch in order to
// allow rendering labels outside of a polygon's
// area. The default behaviour for this is to limit the
// label to inside the polygon.
import Text from "ol/style/Text";
import { applyStyle as applyStyleFunction } from "ol-mapbox-style";
import { latest as spec } from "@mapbox/mapbox-gl-style-spec";

// for JSONP support
import request from "reqwest";

Text.prototype.getOverflow = () => {
  return true;
};

/** Create the parameters for a Vector layer.
 *
 */
function defineSource(mapSource) {
  if (mapSource.type === "wfs") {
    // add a wfs type source
    let format = GML2Format;
    let outputFormat = "text/xml; subtype=gml/2.1.2";

    if (mapSource.params.outputFormat) {
      outputFormat = mapSource.params.outputFormat;
    }

    if (outputFormat.toLowerCase().indexOf("json") >= 0) {
      format = GeoJSONFormat;
    }

    return {
      format: new format({}),
      projection: "EPSG:4326",
      url: function (extent) {
        if (typeof mapSource.params.typename === "undefined") {
          console.error('No "typename" param defined for a WFS layer. This will fail.');
        }

        const urlParams = Object.assign(
          {},
          {
            srsname: "EPSG:3857",
            outputFormat: outputFormat,
            service: "WFS",
            version: "1.1.0",
            request: "GetFeature",
            bbox: extent.concat("EPSG:3857").join(","),
          },
          mapSource.params
        );

        return joinUrl(mapSource.urls[0], urlParams);
      },
      strategy: bbox,
    };
  } else if (mapSource.type === "geojson") {
    return {
      format: new GeoJSONFormat(),
      projection: mapSource.params.crs ? mapSource.params.crs : "EPSG:4326",
      url: mapSource.urls[0],
    };
  } else if (mapSource.type === "ags-vector") {
    // Add an A**GIS FeatureService layer.
    // This performs a basic query based on the bounding box.
    return {
      loader: function (extent) {
        const url = mapSource.urls[0] + "/query/";

        // the E**I language can get a bit complicated but
        //  this is the format for a basic BBOX query.
        const params = Object.assign(
          {},
          {
            f: "json",
            returnGeometry: "true",
            spatialRel: "esriSpatialRelIntersects",
            geometry: JSON.stringify({
              xmin: extent[0],
              ymin: extent[1],
              xmax: extent[2],
              ymax: extent[3],
            }),
            spatialReference: JSON.stringify({
              wkid: 102100,
            }),
            geometryType: "esriGeometryEnvelope",
            inSR: 102100,
            outSR: 102100,
            outFields: "*",
            returnIdsOnly: true,
          },
          mapSource.params
        );

        // use JSONP to fetch the features.
        request({
          url: url,
          data: params,
          type: "jsonp",
          success: (response) => {
            if (response.error) {
              console.error("Error loading object IDs for " + mapSource.label);
            } else {
              const esriFormat = new EsriJsonFormat();

              const objectIds = response.objectIds;
              if (objectIds !== null) {
                // E**I FeatureServer appears to have a limitation of
                // roughly 2048 bytes past the "?" in the GET call.
                //  this ensure that the batches are small enough to prevent
                //  404 errors.
                const nObjects = objectIds.length;

                // dummy "request" that can be used to estimate the
                //  overall length of the query.
                const sniffRequest = Object.assign({}, params, {
                  returnIdsOnly: false,
                });

                // the ID's need to get "batched" into groups
                //  which will reliably be shorter than the
                //  maximum request length.
                const batches = [];

                // current batch is "rotated" whenever it gets too long
                let currentBatch = [];
                for (let i = 0; i < nObjects; i++) {
                  // estimate the request size
                  const requestimate = requEstimator(
                    Object.assign(sniffRequest, {
                      objectIds: currentBatch.concat([objectIds[i]]).join(","),
                    })
                  );
                  // if it's too long "rotate" the batches.
                  if (requestimate > 2000) {
                    batches.push(currentBatch.slice());
                    currentBatch = [];
                  }
                  currentBatch.push(objectIds[i]);
                }
                // ensure the remainder gets pushed to a batch
                if (currentBatch.length > 0) {
                  batches.push(currentBatch);
                }

                // make the feature requests based on the batches.
                for (const batch of batches) {
                  request({
                    url: url,
                    data: Object.assign({}, params, {
                      returnIdsOnly: false,
                      objectIds: batch.join(","),
                    }),
                    type: "jsonp",
                    success: (response) => {
                      if (response.error) {
                        console.error("Error loading feature batch for " + mapSource.label);
                      } else {
                        this.addFeatures(
                          esriFormat.readFeatures(response, {
                            // featureProjection: proj,
                          })
                        );
                      }
                    },
                  });
                }
              }
            }
          },
          error: (response) => {
            console.error(response);
          },
        });
      },
      strategy: tile(
        createXYZ({
          tileSize: 512,
        })
      ),
    };
  }
  // empty object
  return {};
}

/** Return a style function for the layer.
 *
 */
export function applyStyle(vectorLayer, mapSource, mapTool) {
  // if this is a special-case layer, handle it.
  if (mapSource.name === EDIT_LAYER_NAME) {
    const renderPoints = mapTool === "Modify" || mapTool === "_Modify";
    vectorLayer.setStyle(getEditStyle(mapSource.layers[0].style, renderPoints));
    return;
  }

  const layers = [];
  for (const layer of mapSource.layers) {
    if (layer.on === true) {
      // check for the different types of mapbox-gl layers
      //  GeoMoose lets the user define all the properties at once!
      const order = ["fill", "line", "circle", "symbol"];

      for (let o = 0, oo = order.length; o < oo; o++) {
        const glLayerType = order[o];
        let filterFn = (n) => n.indexOf(glLayerType) === 0;
        if (glLayerType === "symbol") {
          filterFn = (n) => n.indexOf("symbol") === 0 || n.indexOf("text") === 0;
        }
        const validKeys = Object.keys(layer.style).filter(filterFn);
        if (validKeys.length > 0) {
          const layerDef = {
            id: `${layer.name}-${glLayerType}`,
            type: glLayerType,
            // source is a constant that is used to dummy up
            //  the Mapbox styles
            source: "dummy-source",
            paint: {},
            layout: {},
          };

          // TODO: if updates are slow then this likely needs
          //  optimized with real for loops.
          ["paint", "layout"].forEach((section) => {
            Object.keys(spec[`${section}_${glLayerType}`]).forEach((key) => {
              if (layer.style[key]) {
                layerDef[section][key] = layer.style[key];
              }
            });
          });

          // check to see if there is a filter
          //  set on the layer.  This uses the Mapbox GL/JS
          //  filters.
          if (layer.filter !== undefined) {
            layerDef.filter = layer.filter;
          }

          layers.push(layerDef);
        }
      }
    }
  }

  applyStyleFunction(
    vectorLayer,
    {
      version: 8,
      layers: layers,
      sources: {
        "dummy-source": {
          type: "vector",
        },
      },
    },
    "dummy-source"
  );
}

/** Return an OpenLayers Layer for the Vector source.
 *
 *  @param mapSource The MapSource definition from the store.
 *
 *  @returns OpenLayers Layer instance.
 */
export function createLayer(mapSource, styleLayer = applyStyle) {
  const source = new VectorSource(defineSource(mapSource));

  // get the transforms for the layer
  if (mapSource.transforms) {
    source.on("addfeature", function (evt) {
      const f = evt.feature;
      f.setProperties(transformProperties(mapSource.transforms, f.getProperties()));
    });
  }

  const opts = {
    source,
    minResolution: mapSource.minresolution,
    maxResolution: mapSource.maxresolution,
    declutter: parseBoolean((mapSource.config || {}).declutter),
  };
  const vectorLayer = new VectorLayer(opts);
  styleLayer(vectorLayer, mapSource);

  return vectorLayer;
}

/** Ensure that the Vector parameters all match.
 */
export function updateLayer(map, layer, mapSource, mapTool, styleLayer = applyStyle) {
  if (mapSource.type === "vector" || mapSource.type === "measure") {
    // vector layer features are defined by what
    // is stored in mapSource.features
    const layerVersion = layer.get("featuresVersion");
    // check to see if there was an update to the features
    if (layerVersion !== mapSource.featuresVersion) {
      // this is a bit heavy-handed strategy.
      const source = layer.getSource();
      // clear the layer without setting off events.
      source.clear(true);
      // setup the JSON parser
      const outputFormat = new GeoJSONFormat();
      // bring in the new features.
      const features = outputFormat.readFeatures({
        type: "FeatureCollection",
        features: mapSource.features,
      });
      source.addFeatures(features);

      // update the version number
      layer.set("featuresVersion", mapSource.featuresVersion);
    }
  } else {
    const layerVersion = layer.get("featuresVersion");
    // check to see if there was an update to the features
    if (layerVersion !== mapSource.featuresVersion) {
      layer.set("featuresVersion", mapSource.featuresVersion);
      // refresh is available to all vector layers,
      //  this should force a reload of the features from the server
      //  upon update.
      layer.getSource().refresh();
    }
  }

  // update the style effectively turns "layers"
  // on and off on vector layers.
  styleLayer(layer, mapSource, mapTool);
}
