import React from 'react'
import PropTypes from 'prop-types'
import nanoid from 'nanoid'
import debounce from 'lodash.debounce'

import MapLogo from './MapLogo'
import {
  createMap,
  updateMapFromUrl,
  updateUrlFromMap,
  replaceZoomBoxCSS,
  addSelectInteraction
} from './utils'
import { StyledMap } from './styled'
import { connectToContext } from 'Provider'
import en from 'locales/en'
import ugh from 'ugh'
import olInteractionSelect from 'ol/interaction/Select'

/**
 * A Reactified ol.Map wrapper component
 * @component
 * @category Map
 * @since 0.1.0
 */
class Map extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      mapInitialized: false
    }

    // map is passed as a prop- use this flag to determine whether a map/portal should be created
    this.passedMap = props.map

    // this is used to create a unique identifier for the map div
    this.target = props.id || `_ol_kit_map_${nanoid(6)}`
    if (this.passedMap) {
      // override target with openlayers map DOM target
      this.target = this.passedMap.getTarget()
    }
  }

  componentDidMount () {
    const {
      addMapToContext,
      contextProps,
      dragZoomboxStyle,
      isMultiMap,
      map: passedMap,
      onMapInit,
      synced,
      translations,
      updateUrlDebounce,
      updateUrlFromView,
      updateViewFromUrl,
      urlViewParam,
      visible
    } = this.props

    // if no map was passed, create the map
    this.map = !this.passedMap
      ? createMap({ isSyncableMap: isMultiMap, synced, target: this.target, visible })
      : passedMap

    if (this.passedMap && !this.passedMap.getTargetElement()) {
      ugh.warn('A `map` prop has been passed to `<Map>` but the openlayers map has not been mounted to the DOM!')
    }

    // setup select interactions for the map
    this.initializeSelect(this.map)

    // optionally add zoombox styling
    replaceZoomBoxCSS(dragZoomboxStyle)

    // callback for <Provider> if it is mounted as hoc
    let mapConfig = {
      map: this.map,
      mapId: this.target,
      selectInteraction: this.selectInteraction,
      translations, // this can be hoisted to <Provider> only in the future
      ...contextProps
    }
    const onMapReady = map => {
      const allSystemsGo = () => {
        addMapToContext(mapConfig)
        this.setState({ mapInitialized: true })
      }
      // pass map back via callback prop
      const initCallback = onMapInit(map)
      // if onMapInit prop returns a promise, render children after promise is resolved
      const isPromise = !!initCallback && typeof initCallback.then === 'function'

      // update AFTER onMapInit to get map into the state/context
      isPromise
        ? initCallback
          .then((res = {}) => {
            const { contextProps = {} } = res

            // result of onMapInit may contain contextProps
            mapConfig = {
              ...mapConfig,
              ...contextProps
            }
          })
          .catch(e => ugh.error('Error caught in \'onMapInit\'', e))
          .finally(allSystemsGo) // always initialize app
        : allSystemsGo()
    }

    // optionally attach map listener
    if (updateUrlFromView) {
      const setUrl = () => updateUrlFromMap(this.map, urlViewParam)
      const mapMoveListener = debounce(setUrl, updateUrlDebounce)

      // update the url param after map movements
      this.map.on('moveend', mapMoveListener)
    }

    // optionally update map view from url param
    if (updateViewFromUrl) {
      // read the url to update the map from view info
      updateMapFromUrl(this.map, urlViewParam)
        .catch(ugh.error)
        .finally(() => onMapReady(this.map)) // always fire callback with map reference on success/failure
    } else {
      // callback that returns a reference to the created map
      onMapReady(this.map)
    }
  }

  initializeSelect = map => {
    const { selectInteraction } = this.props

    // check the map to see if select interaction has been added
    const selectInteractionOnMap = map.getInteractions().getArray()
      // Layer panel also adds a select interaction
      .filter(interaction => interaction._ol_kit_origin !== '_ol_kit_layer_panel_hover')
      // this checks if the select interaction created or passed in is the same instance on the map and never double adds
      .find(interaction => interaction instanceof olInteractionSelect)

    if (selectInteraction) {
      // if select is passed as a prop always use that one first
      this.selectInteraction = selectInteraction

      // do not double add the interaction to the map
      if (!selectInteractionOnMap) map.addInteraction(this.selectInteraction)
    } else {
      // otherwise create a new select interaction
      const { select } = addSelectInteraction(map)

      ugh.warn('<Map> has created a default select interaction (you can use getSelectInteraction(map) to access it). To have ol-kit use your custom select interaction, pass `selectInteraction` as a prop to <Map>.')

      this.selectInteraction = select
    }
  }

  render () {
    const { children, fullScreen, logoPosition, style, translations } = this.props
    const { mapInitialized } = this.state

    return (
      <>
        {!this.passedMap &&
          <StyledMap
            id={this.target}
            fullScreen={fullScreen}
            style={style}>
            <MapLogo logoPosition={logoPosition} translations={translations} />
          </StyledMap>
        }
        {mapInitialized // wait for map to initialize before rendering children
          ? children
          : null
        }
      </>
    )
  }
}

Map.defaultProps = {
  addMapToContext: () => {},
  contextProps: {},
  dragZoomboxStyle: { backgroundColor: 'rgb(0, 50, 50, 0.5)' },
  fullScreen: false,
  isMultiMap: false,
  logoPosition: 'right',
  map: null,
  onMapInit: () => {},
  updateUrlDebounce: 400,
  updateUrlFromView: true,
  updateViewFromUrl: true,
  urlViewParam: 'view',
  style: {},
  synced: true,
  translations: en,
  visible: true
}

Map.propTypes = {
  /** callback passed by a <Provider> parent to attach props from this <Map> (map, selectInteraction, etc.) to context */
  addMapToContext: PropTypes.func,
  /** any ol-kit children components will automatically be passed a reference to the map object via the `map` prop */
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node
  ]),
  /** custom props that get added to Provider context and passed to connectToContext components */
  contextProps: PropTypes.object,
  /** apply styles to the OL shift-zoom box */
  dragZoomboxStyle: PropTypes.object,
  /** if this is set to false, the map will fill it's parent container */
  fullScreen: PropTypes.bool,
  /** optional id to set on openlayers map and htmk id that map renders into (defaulted to unique id internally) */
  id: PropTypes.string,
  /** flag passed for <MultiMapManager> to recognize when <Map> is in multi-map mode */
  isMultiMap: PropTypes.bool,
  /** place the ol-kit logo on the 'left', 'right', or set to 'none' to hide */
  logoPosition: PropTypes.string,
  /** optionally pass a custom map */
  map: PropTypes.object,
  /** callback called with initialized map object after optional animations complete
    note: if a Promise is returned from this function, Map will wait for onMapInit to resolve before rendering children
  */
  onMapInit: PropTypes.func,
  /** the length of debounce on map movements before the url gets updated */
  updateUrlDebounce: PropTypes.number,
  /** add map location coords + zoom level to url as query params */
  updateUrlFromView: PropTypes.bool,
  /** update map view based off the url param */
  updateViewFromUrl: PropTypes.bool,
  /** change the url param used to set the map location coords */
  urlViewParam: PropTypes.string,
  /** an openlayers select interaction passed down to connected components - created internally if not passed as prop */
  selectInteraction: PropTypes.object,
  /** apply inline styles to the map container */
  style: PropTypes.object,
  /** (only used with isMultiMap) sets initial synced state for a SyncableMap */
  synced: PropTypes.bool,
  /** object of string key/values (see: locales) */
  translations: PropTypes.object,
  /** (only used with isMultiMap) sets initial visibility state for a SyncableMap */
  visible: PropTypes.bool
}

export default connectToContext(Map)