import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import { transform } from 'ol/proj'
import olInteractionSelect from 'ol/interaction/Select'
import olFill from 'ol/style/Fill'
import olCircle from 'ol/style/Circle'
import olStyle from 'ol/style/Style'
import olStroke from 'ol/style/Stroke'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { extend, createEmpty } from 'ol/extent'
import qs from 'qs'

import ugh from 'ugh'
import { SyncableMap } from 'classes'

const OLKIT_ZOOMBOX_ID = '_ol-kit-css-zoombox-style'
const DEFAULT_SELECT_NAME = '_ol_kit_default_select'
const DEFAULT_SELECT_STYLE = new olStyle({
  stroke: new olStroke({
    color: 'cyan',
    width: 3
  }),
  image: new olCircle({
    radius: 5,
    fill: new olFill({
      color: '#ffffff'
    }),
    stroke: new olStroke({
      color: 'cyan',
      width: 2
    })
  }),
  fill: new olFill({
    color: 'rgba(255,255,255,0.01)'
  })
})

export function replaceZoomBoxCSS (dragStyle) {
  const exists = document.getElementById(OLKIT_ZOOMBOX_ID)
  const dragStyleString = Object.entries(dragStyle)
    .map(([k, v]) => `${k}:${v}`)
    .join(';')
    .replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)

  if (!exists) {
    const style = window.document.createElement('style')

    style.id = OLKIT_ZOOMBOX_ID
    style.textContent = `.ol-box{ ${dragStyleString} }`

    window.document.head.append(style)
  }
}

/**
 * Create an openlayers map
 * @function
 * @category Map
 * @since 0.1.0
 * @param {Object} [opts] - Object of optional params
 * @param {String} [opts.isSyncableMap] - flag to return a SyncableMap class for multi-map implementations
 * @param {String} [opts.target] - html id tag that map will into which the map will render
 * @returns {ol.Map} A newly constructed [ol.Map]{@link https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html}
 */
export function createMap (opts = {}) {
  if (!opts.target) return ugh.throw('You must pass an options object with a DOM target for the map')
  if (typeof opts.target !== 'string' && opts.target instanceof Element !== true) return ugh.throw('The target should either by a string id of an existing DOM element or the element itself')

  const MapClass = opts.isSyncableMap ? SyncableMap : Map
  // create a new map instance
  const map = new MapClass({
    view: new View({
      center: [-10686671.119494, 4721671.569715], // centered over US in EPSG:3857
      zoom: 5
    }),
    layers: [
      new TileLayer({
        className: '_ol_kit_basemap_layer',
        _ol_kit_basemap: 'osm', // used by BasemapManager
        source: new OSM()
      })
    ],
    controls: [],
    ...opts
  })

  return map
}

/**
 * Update the url with map location coordinates, zoom level & rotation
 * @function
 * @category Map
 * @since 0.1.0
 * @param {ol.Map} map - reference to the openlayers map object
 * @param {String} viewParam - the query param that will be used to update the url with view info
 * @returns {String} The url that is set within the function
 */
export function updateUrlFromMap (map, viewParam = 'view') {
  if (!(map instanceof Map)) return ugh.throw('\'updateUrlFromMap\' requires a valid openlayers map as the first argument')
  const query = qs.parse(window.location.search, { ignoreQueryPrefix: true })
  const coords = transform(map.getView().getCenter(), map.getView().getProjection().getCode(), 'EPSG:4326')
  const view = { [viewParam]: `${parseFloat(coords[1]).toFixed(6)},${parseFloat(coords[0]).toFixed(6)},${parseFloat(map.getView().getZoom()).toFixed(2)},${parseFloat(map.getView().getRotation()).toFixed(2)}` }
  const newQuery = { ...query, ...view }
  const queryString = qs.stringify(newQuery, { addQueryPrefix: true, encoder: (str) => str })
  const newUrl = window.location.pathname + queryString

  window.history.replaceState(null, '', newUrl)

  return newUrl
}

/**
 * Update the map view with location coordinates, zoom level & rotation from the url
 * @function
 * @category Map
 * @since 0.1.0
 * @param {ol.Map} map - reference to the openlayers map object
 * @param {String} viewParam - the query param that will be read to update the map position
 * @returns {Promise} Resolved with transformed center coords after the map has been updated by url info
 */
export function updateMapFromUrl (map, viewParam = 'view') {
  const promise = new Promise((resolve, reject) => {
    const query = qs.parse(window.location.search, { ignoreQueryPrefix: true })

    if (!(map instanceof Map)) return ugh.throw('\'updateMapFromUrl\' requires a valid openlayers map as the first argument', reject)
    if (!query[viewParam]) return ugh.warn(`url param "${viewParam}" was not found on page load`, resolve)
    const [y, x, zoom, rotation] = query[viewParam].split(',')
    const centerAndZoomOpts = { x, y, zoom }
    const coords = centerAndZoom(map, centerAndZoomOpts)

    // wait for the map to update then resolve promise
    map.once('postrender', () => resolve(coords))
    map.getView().animate({
      rotation,
      duration: 0
    })
  })

  return promise
}

/**
 * Update the map location to provided zoom & x, y coordinates
 * @function
 * @category Map
 * @since 0.1.0
 * @param {ol.Map} map - reference to the openlayers map object
 * @param {Object} opts - include x, y, & zoom options
 * @returns {Array} Coordinates used to update the map
 */
export function centerAndZoom (map, opts = {}) {
  if (!(map instanceof Map)) return ugh.throw('\'centerAndZoom\' requires a valid openlayers map as the first argument')
  const transformedCoords = transform([Number(opts.x), Number(opts.y)], 'EPSG:4326', map.getView().getProjection().getCode())

  map.getView().setCenter(transformedCoords)
  map.getView().setZoom(opts.zoom)

  return transformedCoords
}

/**
 * Convert an XY pair to lat/long
 * @function
 * @category Map
 * @since 0.16.0
 * @param {ol.Map} map - reference to the openlayers map object
 * @param {Number} x - the x coordinate
 * @param {Number} y - the x coordinate
 * @returns {Object} An object containing a `longitude` and `latitude` property
 */
export function convertXYtoLatLong (map, x, y) {
  const coords = map.getCoordinateFromPixel([x, y])
  const transformed = transform(coords, map.getView().getProjection().getCode(), 'EPSG:4326')
  const longitude = Number((Number(transformed[0] || 0) % 180).toFixed(6))
  const latitude = Number((transformed[1] || 0).toFixed(6))

  return {
    longitude,
    latitude
  }
}

/**
 * Create a new openlayers select interaction with default styling
 * @function
 * @category Map
 * @since 0.2.0
 * @param {Object} opts - select interaction opts
 * @returns {ol.interaction.Select} https://openlayers.org/en/latest/apidoc/module-ol_interaction_Select-Select.html
 */
export function createSelectInteraction (opts = {}) {
  return new olInteractionSelect({
    hitTolerance: 3,
    style: [DEFAULT_SELECT_STYLE],
    ...opts
  })
}

/**
 * Create a new openlayers select interaction with default styling and add the vector layer to the map
 * @function
 * @category Map
 * @since 1.11.0
 * @param {ol.Map} map - reference to the openlayers map object
 * @param {String} name - identifier to set as _ol_kit_origin on the interaction
 * @param {Object} opts - select interaction opts
 * @returns {Object} object - { layer: {ol.layer.Vector} https://openlayers.org/en/latest/apidoc/module-ol_layer_Vector-VectorLayer.html, select: {ol.interaction.Select} https://openlayers.org/en/latest/apidoc/module-ol_interaction_Select-Select.html }
 */
export function addSelectInteraction (map, name = DEFAULT_SELECT_NAME, opts = {}) {
  if (!(map instanceof Map)) return ugh.throw('\'addSelectInteraction\' requires a valid openlayers map as the first argument')

  const select = createSelectInteraction(opts)
  const source = new VectorSource({ features: select.getFeatures() })
  const layer = new VectorLayer({ source, style: [DEFAULT_SELECT_STYLE], map })

  layer.set('_ol_kit_origin', name)
  select.set('_ol_kit_origin', name)

  map.addInteraction(select)

  return { layer, select }
}

/**
 * Find a select interaction on the map by name (defaults to internal select created by ol-kit if name not passed)
 * @function
 * @category Map
 * @since 1.11.0
 * @param {ol.Map} map - reference to the openlayers map object
 * @param {String} name - identifier to find _ol_kit_origin on the select interaction
 * @returns {Object} object - { layer: {ol.layer.Vector} https://openlayers.org/en/latest/apidoc/module-ol_layer_Vector-VectorLayer.html, select: {ol.interaction.Select} https://openlayers.org/en/latest/apidoc/module-ol_interaction_Select-Select.html }
 */
export function getSelectInteraction (map, name = DEFAULT_SELECT_NAME) {
  if (!(map instanceof Map)) return ugh.throw('\'getSelectInteraction\' requires a valid openlayers map as the first argument')

  const interactions = map.getInteractions().getArray()
  const select = interactions.find(interaction => interaction instanceof olInteractionSelect && interaction.get('_ol_kit_origin') === name)

  if (!select) return ugh.throw(`Select interaction with name '${name}' could not be found on the map`)

  return select
}

/**
 * Sets the map extent to the given values.
 * @function
 * @category Map
 * @param {ol.Map} map Open Layers map
 * @param {ol.Extent} extent New extent definition
 */
export function setMapExtent (map, extent) {
  if (!(map instanceof Map)) return ugh.throw('\'setMapExtent\' requires a valid openlayers map as the first argument')
  map.getView().fit(extent, map.getSize())
}

/**
 * Calculates a map extent that would make all features in the list visible.
 * @function
 * @category Map
 * @param {ol.Feature[]} featureList List of Open Layers features
 * @returns {ol.Extent} Map extent
 */
export function getExtentForFeatures (featureList) {
  const extent = featureList.reduce((acc, f) => extend(acc, f.getGeometry().getExtent()), createEmpty())

  return extent
}