<template>
  <div class="map-layout" ref="mapLayout">
    <MglMap v-if="showMap && mapStyle" :mapStyle="mapStyle" :bounds.sync="bounds" :fitBoundsOptions="fitBoundsOptions" :center="center" :zoom="zoom" :dragRotate="false" :attributionControl="false" class="map" @load="onMapLoaded" @click="onMapClicked" @contextmenu="onMapRightClicked">
      <MglGeolocateControl :positionOptions="{ enableHighAccuracy: true }" :fitBoundsOptions="{ maxZoom: 12.5 }" :trackUserLocation="true" position="top-right" />
      <MglNavigationControl position="top-right" :showCompass="false" />
      <MglScaleControl position="bottom-left" />
      <MglAttributionControl :compact="$mq.mobile" position="bottom-right" />

      <!-- Note: these are not true Mapbox GL controls that get added via addControl(), as those don't mix well with Vue.js templating.
           Instead, we simply put all our custom non-Mapbox controls in the top left corner where they don't clash with any builtin controls. -->
      <div class="mapboxgl-ctrl-top-left">
        <MapFilterControl ref="filterControl" position="top-left" @startFiltering="filtering = true" @stopFiltering="filtering = false" />
        <MapOptionsControl ref="optionsControl" position="top-left" />
        <MapDownloadControl position="top-left" />
      </div>

      <MglPopup v-if="loadingPopupCoordinates" key="loading" :coordinates="loadingPopupCoordinates" :showed="true" anchor="bottom" @added="onPopupAdded">
        <div class="loading-ring-wrapper">
          <LoadingRing />
        </div>
      </MglPopup>

      <SummitPopup v-if="summit" :summit="summit" :lastSpot="lastSummitSpot" :nextAlert="nextSummitAlert" @close="onPopupClosed" />

      <MapRoute v-for="route in persistentRoutes" :key="route.id" :route="route" />

      <MapInfoPopup v-if="infoCoordinates !== null" :coordinates="infoCoordinates" @close="infoCoordinates = null" />

      <MapDraw ref="draw" />

      <MapWebcams v-if="mapOptions.webcams" />
    </MglMap>
    <div v-if="browserNotSupported" class="browser-not-supported">Your browser does not support WebGL, which is required to render this map. <strong>iOS 17 users: There is a bug in iOS 17 that can sometimes cause this error. Restarting Safari (closing/killing it completely) resolves the issue temporarily.</strong></div>
    <div v-if="zoomWarning" class="zoom-warning">Zoom in to see all filtered/spotted summits</div>
    <SwisstopoInfo />
    <b-loading :is-full-page="false" :active="filtering || !showMap || !mapStyle" />
  </div>
</template>

<script>
import axios from 'axios'
import mapboxgl from 'mapbox-gl'
import utils from '../mixins/utils.js'
import smptracks from '../mixins/smptracks.js'
import mapstyle from '../mixins/mapstyle.js'
import longtouch from '../mixins/longtouch.js'

import { MglMap, MglPopup, MglNavigationControl, MglGeolocateControl, MglScaleControl, MglAttributionControl } from 'vue-mapbox'
import MapFilterControl from '../components/MapFilterControl.vue'
import MapOptionsControl from '../components/MapOptionsControl.vue'
import MapDownloadControl from '../components/MapDownloadControl.vue'
import LoadingRing from '../components/LoadingRing.vue'
import SummitPopup from '../components/SummitPopup.vue'
import MapRoute from '../components/MapRoute.vue'
import MapInfoPopup from '../components/MapInfoPopup.vue'
import MapDraw from '../components/MapDraw.vue'
import MapWebcams from '../components/MapWebcams.vue'
import SwisstopoInfo from '../components/SwisstopoInfo.vue'

export default {
  name: 'Map',
  components: {
    MglMap, MglPopup, MglNavigationControl, MglGeolocateControl, MglScaleControl, MglAttributionControl, MapFilterControl, MapOptionsControl, MapDownloadControl, LoadingRing, SummitPopup, MapRoute, MapInfoPopup, MapDraw, MapWebcams, SwisstopoInfo
  },
  mixins: [utils, smptracks, mapstyle, longtouch],
  created () {
    this.map = null
  },
  mounted () {
    if (!mapboxgl.supported()) {
      this.browserNotSupported = true
    }

    // Check for summit code or coordinates first; if present, start map right there
    if (this.$route.params.summitCode) {
      this.fetchSummit(this.$route.params.summitCode)
        .then(summit => {
          if (summit) {
            this.bounds = [[summit.coordinates.longitude, summit.coordinates.latitude], [summit.coordinates.longitude, summit.coordinates.latitude]]
            this.fitBoundsOptions = this.makeFitBoundsOptions()
          }
          this.showMap = true
        })
    } else if (this.$route.params.coordinates) {
      this.center = this.$route.params.coordinates.split(/,/).reverse()
      if (this.$route.params.zoom) {
        this.zoom = parseFloat(this.$route.params.zoom)
      }
      this.showMap = true
    } else if (this.$route.params.region) {
      this.getRegionBounds(this.$route.params.region)
        .then(bounds => {
          this.bounds = bounds
          this.fitBoundsOptions = { animate: false, padding: { left: 60, top: 40, right: 60, bottom: 40 } }
          this.showMap = true
        })
    } else {
      if (localStorage.getItem('bounds')) {
        try {
          this.bounds = JSON.parse(localStorage.getItem('bounds'))
        } catch (e) {}
        this.showMap = true
      } else {
        axios.get(process.env.VUE_APP_API_URL + '/my_coordinates')
          .then(response => {
            if (response.data.latitude && response.data.longitude) {
              this.center = [response.data.longitude, response.data.latitude]
              this.zoom = 8
            } else {
              this.bounds = [[186, 75], [-172, -59]]
            }
          })
          .finally(() => {
            this.showMap = true
          })
      }
    }
  },
  props: {
    clickFuzz: {
      type: Number,
      default: 10
    }
  },
  data () {
    return {
      showMap: false,
      bounds: undefined,
      fitBoundsOptions: undefined,
      center: undefined,
      zoom: 14,
      loadingPopupCoordinates: null,
      summit: null,
      leavingRoute: false,
      zoomWarning: false,
      browserNotSupported: false,
      filtering: false,
      infoCoordinates: null,
      persistentRoutes: []
    }
  },
  watch: {
    bounds (val) {
      localStorage.setItem('bounds', JSON.stringify(Array.isArray(val) ? val : val.toArray()))
      this.updateMapURL()
      if (this.map) {
        this.zoomWarning = (this.map.getZoom() < 3 && (this.$refs.filterControl.isActive() || this.$refs.optionsControl.spotsShown()))
      }
    },
    '$route' (to, from) {
      this.updateRoute()
    },
    summit () {
      if (!this.summit) {
        return
      }
      if (!this.isSummitValid(this.summit)) {
        this.$refs.optionsControl.showInactive()
      }
      this.persistentRoutes = []
    },
    routes () {
      if (this.routes.length !== 0) {
        this.persistentRoutes = this.routes
      }
    },
    mapOptions: {
      handler (newValue) {
        this.updateLayers(this.map)
      },
      deep: true
    }
  },
  computed: {
    lastSummitSpot () {
      if (!this.summit) {
        return null
      }

      let spots = this.$store.state.spots.filter(spot => {
        return (spot.summit.code === this.summit.code)
      }).sort((a, b) => {
        if (a.timeStamp > b.timeStamp) {
          return -1
        } else if (a.timeStamp < b.timeStamp) {
          return 1
        } else {
          return 0
        }
      })
      if (spots.length > 0) {
        return spots[0]
      } else {
        return null
      }
    },
    nextSummitAlert () {
      if (!this.summit) {
        return null
      }

      let alerts = this.$store.state.alerts.filter(alert => {
        return (alert.summit.code === this.summit.code)
      }).sort((a, b) => {
        if (a.dateActivated > b.dateActivated) {
          return 1
        } else if (a.dateActivated < b.dateActivated) {
          return -1
        } else {
          return 0
        }
      })
      if (alerts.length > 0) {
        return alerts[0]
      } else {
        return null
      }
    },
    mapOptions () {
      return this.$store.state.mapOptions
    }
  },
  methods: {
    onMapLoaded (event) {
      this.map = event.map
      this.map.touchZoomRotate.disableRotation();
      ['summits_circles_all', 'summits_circles', 'summits_inactive_circles'].forEach(layer => {
        this.map.on('mouseenter', layer, () => {
          if (!this.$refs.draw.isDrawing()) {
            this.map.getCanvas().style.cursor = 'pointer'
          }
        })
        this.map.on('mouseleave', layer, () => {
          this.map.getCanvas().style.cursor = ''
        })
      })

      this.updateLayers(this.map)
      this.map.setLayoutProperty('summits_circles_all', 'visibility', 'visible')

      this.installLongTouchHandler(this.map, (e) => {
        this.infoCoordinates = {
          latitude: e.lngLat.lat,
          longitude: e.lngLat.lng
        }
      })
      this.updateRoute()
    },
    onMapClicked (event) {
      if (this.$refs.draw.isDrawing() || event.mapboxEvent.originalEvent.hitMarker) {
        return
      }

      // Search for summit circles with some padding/fuzz to make it easier to hit on mobile devices
      let point = event.mapboxEvent.point
      let bbox = [[point.x - this.clickFuzz, point.y - this.clickFuzz], [point.x + this.clickFuzz, point.y + this.clickFuzz]]
      let features = this.map.queryRenderedFeatures(bbox, { layers: ['summits_circles_all', 'summits_circles', 'summits_inactive_circles'] })

      if (features.length === 0) {
        // User probably clicked outside any features; close any controls
        this.$refs.filterControl.close()
        this.$refs.optionsControl.close()
        if (!this.summit) {
          this.persistentRoutes = []
        }
        return
      }

      // Find the summit closest to where the user tapped
      let minDistance = null
      let chosenFeature = null
      features.forEach(feature => {
        let projected = this.map.project(feature.geometry.coordinates)
        let distance = Math.pow(projected.x - point.x, 2) + Math.pow(projected.y - point.y, 2)
        if (minDistance === null || distance < minDistance) {
          minDistance = distance
          chosenFeature = feature
        }
      })

      if (chosenFeature) {
        this.handleSummitClick(chosenFeature)
      }
    },
    onMapRightClicked (event) {
      this.infoCoordinates = {
        latitude: event.mapboxEvent.lngLat.lat,
        longitude: event.mapboxEvent.lngLat.lng
      }
    },
    updateRoute () {
      if (!this.$route.path.startsWith('/map') || this.lastSetUrl === this.$route.path) {
        this.lastSetUrl = null
        return
      }

      this.$nextTick(() => {
        this.map.resize()
      })

      if (this.$route.params.summitCode) {
        this.jumpToSummitCode(this.$route.params.summitCode)
        return
      } else if (this.$route.params.coordinates) {
        let coords = this.$route.params.coordinates.split(/,/).reverse()
        this.map.jumpTo({
          center: coords,
          zoom: this.$route.params.zoom ? parseFloat(this.$route.params.zoom) : 14
        })
        if (this.$route.query.popup) {
          this.infoCoordinates = {
            latitude: parseFloat(coords[1]),
            longitude: parseFloat(coords[0])
          }
        }
      } else if (this.$route.params.region) {
        this.getRegionBounds(this.$route.params.region)
          .then(bounds => {
            this.map.fitBounds(bounds, { animate: true, padding: { left: 60, top: 40, right: 60, bottom: 40 } })
          })
      }

      // Additionally a hash with summit code may be provided to open the popup
      let matches = window.location.hash.match('^#/summits/(.+)$')
      if (matches && (this.summit === null || matches[1] !== this.summit.code)) {
        this.fetchSummit(matches[1])
          .then(summit => {
            this.summit = summit
          })
      }
    },
    onPopupClosed () {
      this.summit = null
      this.updateMapURL()
    },
    onPopupAdded (popup) {
      popup.popup.options.focusAfterOpen = false
    },
    handleSummitClick (feature) {
      this.loadingPopupCoordinates = feature.geometry.coordinates
      this.summit = null
      this.fetchSummit(feature.properties.code)
        .then(summit => {
          this.loadingPopupCoordinates = null
          this.summit = summit
          this.updateMapURL()
        })
    },
    fetchSummit (summitCode) {
      return axios.get(process.env.VUE_APP_API_URL + '/summits/' + summitCode)
        .then(response => {
          let summit = response.data
          summit.photo = null
          return summit
        })
    },
    fetchAssociation (associationCode) {
      return axios.get(process.env.VUE_APP_API_URL + '/associations/' + associationCode)
        .then(response => {
          return response.data
        })
    },
    getRegionBounds (region) {
      let assocReg = region.split('/')
      return this.fetchAssociation(assocReg[0])
        .then(association => {
          let region = association.regions.find(el => el.code === assocReg[1])
          if (region) {
            // Add padding to bounds
            let padding = 1 // km
            region.bounds[0][0] -= padding / (Math.cos(region.bounds[0][1] * Math.PI / 180) * 111.32)
            region.bounds[0][1] -= padding / 111.32
            region.bounds[1][0] += padding / (Math.cos(region.bounds[0][1] * Math.PI / 180) * 111.32)
            region.bounds[1][1] += padding / 111.32
            return region.bounds
          }
        })
    },
    jumpToSummitCode (summitCode) {
      if (this.summit != null && summitCode === this.summit.code) {
        return
      }

      this.fetchSummit(summitCode)
        .then(summit => {
          if (!summit) {
            return
          }

          this.jumpToSummit(summit)
        })
    },
    jumpToSummit (summit) {
      this.map.fitBounds([[summit.coordinates.longitude, summit.coordinates.latitude], [summit.coordinates.longitude, summit.coordinates.latitude]], this.makeFitBoundsOptions())
      this.summit = summit
    },
    makeCoordinateLink (summitCode, longitude, latitude) {
      if (summitCode.match(/^HB0?\//)) {
        return 'https://map.geo.admin.ch/?swisssearch=' + latitude + ',' + longitude
      } else {
        return 'https://www.openstreetmap.org/?mlat=' + latitude + '&mlon=' + longitude + '&zoom=14'
      }
    },
    updateMapURL () {
      if (this.map) {
        let url = '/map/coordinates/' + this.map.getCenter().toArray().reverse().map(a => { return a.toFixed(6) }).join(',') + '/' + this.map.getZoom().toFixed(1)
        let hash = ''
        if (this.summit && this.summit.code) {
          hash = '#/summits/' + this.summit.code
        }
        let fullPath = url + hash
        if (!this.leavingRoute && this.$router.currentRoute.fullPath !== fullPath) {
          this.lastSetUrl = url
          this.$router.replace(fullPath)
        }
      }
    },
    makeFitBoundsOptions () {
      let offsetY = 0
      let assumedPopupHeight = 430
      let mapHeight = this.$refs.mapLayout.clientHeight
      if (mapHeight < (assumedPopupHeight * 2)) {
        // Popup probably won't fit the screen vertically if we center the map on the summit, so we shift the map down
        offsetY = Math.max(0, Math.min(assumedPopupHeight - mapHeight / 2, mapHeight / 4 - 20))
      }
      return { offset: [0, offsetY], maxZoom: 14, animate: false }
    }
  },
  provide () {
    return {
      map: this.map
    }
  },
  beforeRouteEnter (to, from, next) {
    next(vm => {
      vm.leavingRoute = false
      document.title = 'Map - SOTLAS'
    })
  },
  beforeRouteLeave (to, from, next) {
    if (!to.path.match(/^\/map/)) {
      this.leavingRoute = true
    }
    next()
  }
}
</script>

<style scoped>
.map-layout {
  position: absolute;
  touch-action: manipulation;
  top: 3.25rem;
  right: 0;
  bottom: 0;
  left: 0;
}
.map >>> .mapboxgl-popup {
  max-width: 600px !important;
}
.loading-ring-wrapper {
  margin: 1rem 0.5rem 0.5rem 0.5rem;
}
.zoom-warning {
  position: absolute;
  left: 50%;
  top: 1.5rem;
  transform: translate(-50%, 0);
  background-color: #feffd2;
  padding: 0.2em 0.5em;
  border-radius: 0.5em;
  text-align: center;
  opacity: 0.9;
}
.browser-not-supported {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: #fdc5c9;
  padding: 0.2em 0.5em;
  border-radius: 0.5em;
}
</style>
