Source: components/viewport/viewport-mixin.js

import View from 'ol/View.js'
import WebGLTile from 'ol/layer/WebGLTile.js'
import VectorLayer from 'ol/layer/Vector'
import GeoTIFF from 'ol/source/GeoTIFF.js'
import VectorSource from 'ol/source/Vector'
import Projection from 'ol/proj/Projection.js'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import { Icon, Style } from 'ol/style'
import { getImageXY, getZ } from '@dataforsyningen/saul'
import { configuration } from '../../modules/configuration.js'
import { state } from '../../state/index.js'
import { renderParcels } from '../../custom-plugins/plugin-parcel.js'

// HACK to avoid bug looking up meters per unit for 'pixels' (https://github.com/openlayers/openlayers/issues/13564)
// when the view resolves view properties, the map view will be updated with the HACKish projection override
const projection = new Projection({
  code: 'custom',
  units: 'pixels',
  metersPerUnit: 1
})

function generateSource(geotiff_href) {

  let gtif
  const searchParams = new URLSearchParams(location.search)

  // TODO: Remove
  // --- POC HACK ---
  // Force GeoTiff image from URL param
  if (searchParams.has('gtif')) {
    gtif = searchParams.get('gtif')
  } else {
    gtif = geotiff_href
  }
  // --- END POC HACK ---

  return new GeoTIFF({
    convertToRGB: true,
    transition: 0,
    sources: [{ url: gtif, bands: [1,2,3] }] // Ignores band 4. See https://openlayers.org/en/latest/apidoc/module-ol_source_GeoTIFF.html#~SourceInfo
  })
}

function generateIconLayer(center) {
  if (center) {
    let icon_feature = new Feature({
      geometry: new Point([center[0], center[1]])
    })
    const colorSetting = configuration.COLOR_SETTINGS.targetColor
    let icon
    if (configuration.ENABLE_CROSSHAIR_ICON) {
      icon = new Icon({
        src: '/img/icons/crosshairs.svg',
        scale: 1,
        anchor: [0.5,0.5]
      })
    } else {
      icon = new Icon({
        src: '/img/icons/pointer-position.svg',
        scale: 1,
        color: colorSetting,
        anchor: [0.5,1]
      })
    }
    const icon_style = new Style({ image: icon })

    icon_feature.setStyle(icon_style)
    const newVectorLayer = new VectorLayer({
      source: new VectorSource({
        features: [icon_feature]
      })
    })
    newVectorLayer.id = 'vectoriconlayer'
    return newVectorLayer
  }
}

function generateLayer(src) {
  const layer = new WebGLTile({source: src, preload: 0})
  layer.id = 'geotifflayer'
  return layer
}

function getLayerById(map, id) {
  const layerCollection = map.getLayers().getArray()
  for (let i = 0; i < layerCollection.length; i++) {
    if (layerCollection[i].id === id) {
      return layerCollection[i]
    }
  }
}

/** Updates the zoom and placement (center) values of a map */
async function updateMapView({map, zoom, center, item}) {
  // Figure out which layer has the GeoTIFF source image
  const geoTiffLayer = getLayerById(map, 'geotifflayer')
  const GeoTIFFsource = geoTiffLayer.getSource()
  // Update view based on source
  const view = await GeoTIFFsource.getView()
  view.projection = projection
  view.resolutions = addResolutions(view.resolutions) // Set extra resolutions so we can zoom in further than the resolutions permit normally
  view.rotation = getAdjustedNadirRotation(item) // Rotate nadir images relative to north
  view.center = center // Set center position in image
  view.zoom = zoom // Set zoom
  const mapView = createView(view)
  map.setView(mapView)
}

/** Handler to update the relevant parts of the image map when item, view, or marker is updated */
async function updateViewport(newData, oldData, map) {
  if (!newData.item || !newData.view || !newData.marker) {
    return
  }

  // On item change, load a new image layer in map and update view/marker
  if (newData.item !== oldData.item) {
    updateMapImage(map, newData.item)
    await updateView(newData, map)
    await updateMarker(newData, map)
    return
  }
  
  // On view change, update map view
  if (newData.view.position !== oldData.view.position || newData.view.zoom !== oldData.view.zoom) {
    await updateView(newData, map)
  }

  // On marker change, update marker position
  if (newData.marker.position !== oldData.marker.position) {
    await updateMarker(newData, map)
  }

  return
}

/** Calculates new view position and updates image map view */
async function updateView(data, map) {
  const newViewCoords = await updateCenter(data.view.position, data.item, data.view.kote)
  await updateMapView({
    map: map,
    item: data.item,
    zoom: data.view.zoom,
    center: newViewCoords.imageCoord
  })
  return
}

/** Calculates new marker position and updates marker in image map */
async function updateMarker(data, map) {
  const newMarkerCoords = await updateCenter(data.marker.position, data.item, data.marker.kote)
  updateMapCenterIcon(map, newMarkerCoords.imageCoord)
  return
}

/** Updates the image displayed in a map */
function updateMapImage(map, item) {
  const layer = getLayerById(map, 'geotifflayer')
  if (layer) {
    map.removeLayer(layer)
  }
  const source_image = generateSource(item.assets.data.href)
  const newLayer = generateLayer(source_image)
  map.addLayer(newLayer)
}

/** Updates the position of the point of interest icon */
function updateMapCenterIcon(map, localCoordinate) {
  map.removeLayer(getLayerById(map, 'vectoriconlayer'))
  let newIconLayer
  newIconLayer = generateIconLayer(localCoordinate)
  map.addLayer(newIconLayer)
}

/** Completely update an image map */
async function updateMap(self) {

  if (!self.item || !self.map) {
    return
  }

  const coords = await updateCenter(state.marker.position, self.item, state.marker.kote)

  // Create and add image layer
  updateMapImage(self.map, self.item)

  // Create icon layer
  updateMapCenterIcon(self.map, coords.imageCoord)

  // Update the map's view
  await updateMapView({
    map: self.map,
    zoom: self.toImageZoom(state.view.zoom),
    center: coords.imageCoord,
    item: self.item
  })
}

/** Adds extra resolutions to enable deep zoom */
function addResolutions(resolutions) {
  let new_resolutions = Array.from(resolutions)
  const tiniest_res = new_resolutions[new_resolutions.length - 1]
  new_resolutions.push(tiniest_res / 2)
  new_resolutions.push(tiniest_res / 4)
  return new_resolutions
}

/** Calculate how much to rotate a nadir image to have it north upwards */
function getAdjustedNadirRotation(item) {
  if (item.properties.direction === 'nadir') {
    return ( item.properties['pers:kappa'] * Math.PI ) / 180
  } else {
    return 0
  }
}

let extentAdjusted = null // Add a flag to track if extent is already adjusted

/** Create a modified View object with min and max zoom levels */
function createView(view_config) {
  if (!extentAdjusted) {
  const extent = view_config.extent // Get the existing extent
  const extentPadding = 0.1 // Adjust this value to control the extent padding

  // Calculate the new extent with padding
  view_config.extent = [
    extent[0] - (extent[2] - extent[0]) * extentPadding, // minx
    extent[1] - (extent[3] - extent[1]) * extentPadding, // miny
    extent[2] + (extent[2] - extent[0]) * extentPadding, // maxx
    extent[3] + (extent[3] - extent[1]) * extentPadding  // maxy
  ] // Set the new extent in the view configuration
  extentAdjusted = false
  } else if (self.item) {
    extentAdjusted = true
  }

  const view = new View(view_config)
  view.setMinZoom(configuration.MIN_ZOOM)
  view.setMaxZoom(configuration.MAX_ZOOM - configuration.MINI_ZOOM_DIFFERENCE)
  return view
}

function updateTextContent(imagedata) {
  let orientationStr
  switch(imagedata.properties.direction) {
    case 'nadir': 
      orientationStr = 'top'
      break
    case 'north': 
      orientationStr = 'syd'
      break
    case 'south': 
      orientationStr = 'nord'
      break
    case 'east': 
      orientationStr = 'vest'
      break
    case 'west': 
      orientationStr = 'øst'
      break
  }
  return `Billede af området omkring koordinat ${ state.marker.position[0].toFixed(0) } Ø, ${ state.marker.position[1].toFixed(0) } N set fra ${ orientationStr }.`
}

function updatePlugins(self, item) {
  if (configuration.ENABLE_PARCEL) {
    renderParcels(self, item.id)
  }
}

function updateDate(imagedata) {
  return new Date(imagedata.properties.datetime).toLocaleDateString()
}

/** Uses world coordinate and image data to calculate an image coordinate */
async function updateCenter(coordinate, item, kote) {
  if (!item) {
    return
  }
  if (kote === undefined || kote === null) {
    kote = await getZ(coordinate[0], coordinate[1], configuration)
  }
  return {
    worldCoord: [...coordinate, kote],
    imageCoord: getImageXY(item, coordinate[0], coordinate[1], kote)
  }
}

export {
  projection,
  updateViewport,
  updateView,
  updateMarker,
  updateMap,
  updateMapView,
  updateMapCenterIcon,
  updateMapImage,
  generateSource,
  generateLayer,
  updateTextContent,
  updatePlugins,
  updateDate,
  updateCenter
}