Source: dumbyUtils.mjs

import LeaderLine from 'leader-line'
import { replaceTextNodes, full2Half } from './utils'
import proj4 from 'proj4'
import { coordPattern, GeoLink } from './Link.mjs'

/**
 * focusNextMap.
 * @param {Boolean} reverse - focus previous map
 */
export function focusNextMap (reverse = false) {
  const renderedList = this.utils.renderedMaps()
  const index = renderedList.findIndex(e => e.classList.contains('focus'))
  const nextIndex = (index + (reverse ? -1 : 1)) % renderedList.length

  const nextMap = renderedList.at(nextIndex)
  nextMap.classList.add('focus', 'focus-manual')
  nextMap.scrollIntoView({ behavior: 'smooth' })
}

/**
 * focusNextBlock.
 *
 * @param {Boolean} reverse - focus previous block
 */
export function focusNextBlock (reverse = false) {
  const blocks = this.blocks.filter(b =>
    b.checkVisibility({
      contentVisibilityAuto: true,
      opacityProperty: true,
      visibilityProperty: true,
    }),
  )
  const index = blocks.findIndex(e => e.classList.contains('focus'))
  const nextIndex = (index + (reverse ? -1 : 1)) % blocks.length

  blocks.forEach(b => b.classList.remove('focus'))
  const nextBlock = blocks.at(nextIndex)
  nextBlock?.classList?.add('focus')
  scrollToBlock(nextBlock)
}

/**
 * scrollToBlock. Smoothly scroll to target block.
 * If block is bigger than viewport, then pick strategy wisely.
 *
 * @param {HTMLElement} block - Scroll to this element
 */
export const scrollToBlock = block => {
  const parentRect = block.parentElement.getBoundingClientRect()
  const scrollBlock =
    block.getBoundingClientRect().height > parentRect.height * 0.8
      ? 'nearest'
      : 'center'
  block.scrollIntoView({ behavior: 'smooth', block: scrollBlock })
}

/**
 * focusDelay. Delay of throttle, value changes by cases
 */
export function focusDelay () {
  return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300
}

/**
 * switchToNextLayout.
 *
 * @param {Boolean} reverse - Switch to previous one
 */
export function switchToNextLayout (reverse = false) {
  const layouts = this.layouts
  const currentLayoutName = this.container.dataset.layout
  const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName)
  const padding = reverse ? -1 : 1
  const nextIndex =
    currentIndex === -1
      ? 0
      : (currentIndex + padding + layouts.length) % layouts.length
  const nextLayout = layouts[nextIndex]
  this.container.dataset.layout = nextLayout.name
}

/**
 * removeBlockFocus.
 */
export function removeBlockFocus () {
  this.blocks.forEach(b => b.classList.remove('focus'))
}

/**
 * addMarkerByPoint.
 *
 * @param {Number[]} options.point - page XY
 * @param {HTMLElement} options.map
 */
export const addMarkerByPoint = ({ point, map, title }) => {
  const rect = map.getBoundingClientRect()
  const [lon, lat] = map.renderer
    .unproject([point[0] - rect.left, point[1] - rect.top])
    .map(value => parseFloat(value.toFixed(6)))

  const marker = map.renderer.addMarker({
    xy: [lon, lat],
  })
  marker.dataset.xy = `${lon},${lat}`
  if (title) marker.title = title

  return marker
}

/**
 * addGeoSchemeByText.
 *
 * @param {Node} node
 */
export const addGeoSchemeByText = async (node) => {
  const digit = '[\\d\\uFF10-\\uFF19]'
  const decimal = '[.\\uFF0E]'
  const coordPattern = `(-?${digit}+${decimal}?${digit}*)`
  const DMSPattern = `([NEWS]${digit}+[dD°度]? ${digit}+[mM'分]? ${digit}+${decimal}?${digit}*[sS"秒]?)`
  const re = new RegExp(`${coordPattern}[,\x2F\uFF0C]${coordPattern}|${DMSPattern}\\s${DMSPattern}`, 'g')

  return replaceTextNodes(node, re, match => {
    const patterns = match.filter(p => p)
    const [match1, match2] = [full2Half(patterns.at(1)), full2Half(patterns.at(2))]
    let [x, y] = [undefined, undefined]

    // Get x,y by DMS or coordinates pattern
    if (match1.match('^[NEWS]')) {
      const dms2degreeString = (pchar, nchar) => (dms) => {
        const matches = dms.match(new RegExp(`([${pchar}${nchar}])(\\d+) (\\d+) (.*)$`))
        if (!matches) return null

        return (matches[1] === pchar ? '' : '-') + (Number(matches[2]) + Number(matches[3]) / 60 + Number(matches[4]) / 3600)
      }
      x = [match1, match2].map(dms2degreeString('E', 'W')).find(x => x)
      y = [match1, match2].map(dms2degreeString('N', 'S')).find(y => y)
    } else {
      [x, y] = [match1, match2]
    }

    // Don't process string which can be used as date
    if (Date.parse(match.at(0) + ' 1990')) return null
    if (!x || !y) return null

    // Return anchor element with Geo Scheme
    const a = document.createElement('a')
    a.className = 'not-geolink from-text'
    a.href = `geo:0,0?xy=${x},${y}`
    a.textContent = match.at(0)
    return a
  })
}

/**
 * @description Add more information into Anchor Element within Geo Scheme by CRS
 * @param {String} crs - EPSG/ESRI Code for CRS
 * @return {Function} - Function for link
 */
export const updateGeoSchemeByCRS = (crs) => (link) => {
  const transform = proj4(crs, 'EPSG:4326')
  const params = new URLSearchParams(link.search)
  let xy = params.get('xy')?.split(',')?.map(Number)

  // Set coords for Geo Scheme
  if (link.href.startsWith('geo:0,0')) {
    if (!xy) return null

    const [lon, lat] = transform.forward(xy)
      .map(value => parseFloat(value.toFixed(6)))
    link.href = `geo:${lat},${lon}`
  }

  const [lat, lon] = link.href
    .match(coordPattern)
    .slice(1)
    .map(Number)

  if (!xy) {
    xy = transform.inverse([lon, lat])
    params.set('xy', xy)
  }

  // set query strings
  params.set('crs', crs)
  params.set('q', `${lat},${lon}`)
  link.search = params

  const unit = proj4(crs).oProj.units
  const invalidDegree = unit === 'degrees' &&
    (lon > 180 || lon < -180 || lat > 90 || lat < -90)
  const invalidMeter = unit === 'm' && xy.find(v => v < 100)
  if (invalidDegree || invalidMeter) {
    link.replaceWith(document.createTextNode(link.textContent))
    return null
  }

  return link
}

/**
 * addGeoLinkByDrag.
 *
 * @param {HTMLElement} container
 * @param {Range} range
 */
export const addGeoLinkByDrag = (container, range, endOfLeaderLine) => {
  // link placeholder when dragging
  container.classList.add('dragging-geolink')
  const link = document.createElement('a')
  link.textContent = range.toString()
  link.classList.add('with-leader-line', 'geolink', 'drag', 'from-text')

  // Replace current content with link
  const originContent = range.cloneContents()
  const resumeContent = () => {
    range.deleteContents()
    range.insertNode(originContent)
  }
  range.deleteContents()
  range.insertNode(link)

  // Add leader-line
  const line = new LeaderLine({
    start: link,
    end: endOfLeaderLine,
    path: 'magnet',
  })

  const positionObserver = new window.MutationObserver(() => {
    line.position()
  })
  positionObserver.observe(endOfLeaderLine, {
    attributes: true,
    attributeFilter: ['style'],
  })

  // Handler for dragend
  container.onmouseup = (e) => {
    container.classList.remove('dragging-geolink')
    container.onmousemove = null
    container.onmouseup = null
    link.classList.remove('drag')
    positionObserver.disconnect()
    line.remove()
    endOfLeaderLine.remove()

    const map = document.elementFromPoint(e.clientX, e.clientY)
      .closest('.mapclay[data-render="fulfilled"]')
    if (!map) {
      resumeContent()
      return
    }

    const marker = addMarkerByPoint({ point: [e.clientX, e.clientY], map })
    if (!marker) {
      resumeContent()
      return
    }

    link.href = `geo:${marker.dataset.xy.split(',').reverse()}`
    GeoLink(link)
  }
}