Source: MenuItem.mjs

import { shiftByWindow } from './utils.mjs'
import { addMarkerByPoint } from './dumbyUtils.mjs'
/* eslint-disable-next-line no-unused-vars */
import { GeoLink, getMarkersFromMaps, removeLeaderLines } from './Link.mjs'
import * as markers from './marker.mjs'
import { parseConfigsFromYaml } from 'mapclay'

/**
 * @typedef {Object} RefLink
 * @property {string} ref - name of link
 * @property {string} link - content of link
 * @property {string|null} title - title of link
 */

/**
   * Creates a Item instance
   *
   * @param {Object} options - The options for the item
   * @param {string} [options.text] - The text content of the item
   * @param {string} [options.innerHTML] - The HTML content of the item
   * @param {string} [options.title] - The title attribute for the item
   * @param {Function} [options.onclick] - The click event handler
   * @param {string} [options.style] - The CSS style string
   * @param {string[]} [options.className] - Additional CSS classes
   */
export const Item = ({
  id,
  text,
  innerHTML,
  title,
  onclick,
  style,
  className,
}) => {
  const menuItem = document.createElement('div')
  if (id) menuItem.id = id
  if (title) menuItem.title = title
  menuItem.innerHTML = innerHTML ?? text
  menuItem.onclick = onclick
  menuItem.style.cssText = style
  menuItem.classList.add('menu-item')
  className?.forEach(c => menuItem.classList.add(c))

  menuItem.onmouseover = () => {
    menuItem.parentElement
      .querySelectorAll('.sub-menu')
      .forEach(sub => sub.remove())
  }
  return menuItem
}

  /**
   * Creates a new menu item that generates a submenu on hover
   *
   * @param {Object} options - The options for the folder
   * @param {string} [options.text] - The text content of the folder
   * @param {string} [options.innerHTML] - The HTML content of the folder
   * @param {Item[]} options.items - The submenu items
   */
  export const Folder = ({ id, text, innerHTML, items, style }) => {
    const folder = document.createElement('div')
    if (id) folder.id = id
    folder.innerHTML = innerHTML ?? text
    folder.classList.add('folder', 'menu-item')
    folder.items = items
    folder.onmouseover = () => {
      if (folder.querySelector('.sub-menu')) return
      // Prepare submenu
      const submenu = document.createElement('div')
      submenu.className = 'sub-menu'
      const offset = folder.items.length > 1 ? '-20px' : '0px'
      submenu.style.cssText = `${style ?? ''}position: absolute; left: 105%; top: ${offset};`
      folder.items.forEach(item => submenu.appendChild(item))
      submenu.onmouseleave = () => {
        if (submenu.querySelectorAll('.sub-menu').length > 0) return
        submenu.remove()
      }

      // hover effect
      folder.parentElement
        .querySelectorAll('.sub-menu')
        .forEach(sub => sub.remove())
      folder.appendChild(submenu)
      shiftByWindow(submenu)
    }
  return folder
  }

export const simplePlaceholder = (text) => Item({
  text,
  style: 'width: fit-content; margin: 0 auto; color: gray; pointer-events: none; font-size: 0.8rem; line-height: 1; font-weight: bolder;',
})

/**
 * Pick up a map
 *
 * @param {Object} options - The options object
 * @param {Object} options.utils - Utility functions
 * @returns {Folder} A Folder instance for picking a map
 */
export const pickMapItem = ({ utils }) =>
  Folder({
    innerHTML: '<span>Maps<span><span class="info">(Tab)</span>',
    items: utils.renderedMaps().map(
      map =>
        Item({
          text: map.id,
          onclick: () => {
            map.classList.add('focus')
            map.scrollIntoView({ behavior: 'smooth' })
          },
        }),
    ),
  })

/**
 * pickBlockItem.
 *
 * @param {HTMLElement[]} options.blocks
 * @param {Function[]} options.utils
 */
export const pickBlockItem = ({ blocks, utils }) =>
  Folder({
    innerHTML: '<span>Blocks<span><span class="info">(n/p)</span>',
    items: blocks.map(
      (block, index) => {
        const focus = block.classList.contains('focus')
        const preview = block.querySelector('p')
          ?.textContent.substring(0, 15)
          ?.concat(' ', '...  ') ?? ''

        return Item({
          className: ['keep-menu', focus ? 'checked' : 'unchecked'],
          innerHTML:
            `<strong>(${index})</strong><span style='display: inline-block; margin-inline: 1.2em;'>${preview}</span>`,
          onclick: (e) => {
            block.classList.toggle('focus')

            const focus = block.classList.contains('focus')
            if (focus) utils.scrollToBlock(block)
            const item = e.target.closest('.menu-item.keep-menu')
            item.classList.add(focus ? 'checked' : 'unchecked')
            item.classList.remove(focus ? 'unchecked' : 'checked')

            // UX: remove menu after user select/deselect blocks
            const submenu = e.target.closest('.sub-menu')
            submenu.onmouseleave = () => { submenu.closest('.menu').style.display = 'none' }
          },
        })
      },
    ),
  })

/**
 * pickLayoutItem.
 *
 * @param {HTEMElement} options.container
 * @param {String[]} options.layouts
 */
export const pickLayoutItem = ({ container, layouts }) =>
  Folder({
    innerHTML: '<span>Layouts<span><span class="info">(x)</span>',
    items: [
      ...layouts.map(
        layout =>
          Item({
            text: layout.name,
            onclick: () => container.setAttribute('data-layout', layout.name),
          }),
      ),
      Item({
        innerHTML: '<a href="https://github.com/outdoorsafetylab/dumbymap#layouts" class="external" style="display: block; padding: 0.5rem;">More...</a>',
        style: 'padding: 0;',
      }),
    ],
  })

/**
 * addGeoLink.
 *
 * @param {Function[]} options.utils
 * @param {Range} range
 */
export const addGeoLink = ({ utils }, range) =>
  Item({
    text: 'Add GeoLink',
    onclick: () => {
      const content = range.toString()
      // FIXME Apply geolink only on matching sub-range
      const match = content.match(/(^\D*[\d.]+)\D+([\d.]+)\D*$/)
      if (!match) return false

      const [x, y] = match.slice(1)
      const anchor = document.createElement('a')
      anchor.textContent = content
      // FIXME apply WGS84
      anchor.href = `geo:${y},${x}?xy=${x},${y}`

      // FIXME
      if (utils.createGeoLink(anchor)) {
        range.deleteContents()
        range.insertNode(anchor)
      }
    },
  })

  /**
   * Suggestion. Menu Item for editor suggestion.
   *
   * @param {String} options.text
   * @param {String} options.replace - new text content
   * @param {CodeMirror} options.cm
   */
  export const Suggestion = ({ text, replace, cm }) => {
    const suggestion = Item({text})
    suggestion.replace = replace
    suggestion.classList.add('suggestion')

    suggestion.onmouseover = () => {
      Array.from(suggestion.parentElement?.children)?.forEach(s =>
        s.classList.remove('focus'),
      )
      suggestion.classList.add('focus')
    }
    suggestion.onmouseout = () => {
      suggestion.classList.remove('focus')
    }
    suggestion.onclick = () => {
      const anchor = cm.getCursor()
      cm.setSelection(anchor, { ...anchor, ch: 0 })
      cm.replaceSelection(suggestion.replace)
      cm.focus()
      const newAnchor = { ...anchor, ch: suggestion.replace.length }
      cm.setCursor(newAnchor)
    }
    return suggestion
  }

/**
 * renderResults. return a menu item for reporting render results
 *
 * @param {Object} options.modal - Ojbect of plain-modal
 * @param {HTMLElement} options.modalContent
 * @param {HTMLElement} map - Rendered map element
 */
export const renderResults = ({ modal, modalContent }, map) =>
  Item({
    text: 'Render Results',
    onclick: () => {
      modal.open()
      modal.overlayBlur = 3
      modal.closeByEscKey = false
      // HACK find another way to override inline style
      document.querySelector('.plainmodal-overlay-force').style.position =
        'relative'

      modalContent.innerHTML = ''
      const sourceCode = document.createElement('div')
      sourceCode.innerHTML = `<a href="${map.renderer.url ?? map.renderer.use}">Source Code</a>`
      modalContent.appendChild(sourceCode)
      const printDetails = result => {
        // const funcBody = result.func.toString()
        // const loc = funcBody.split('\n').length
        const color =
          {
            success: 'green',
            fail: 'red',
            skip: 'black',
            stop: 'chocolate',
          }[result.state] ?? 'black'
        printObject(
          result,
          modalContent,
          `${result.func.name} <span style='float: right;'><span style='display: inline-block; width: 100px; color: ${color};'>${result.state}</span></span>`,
        )
      }

      // Add contents about prepare steps
      const prepareHeading = document.createElement('h3')
      prepareHeading.textContent = 'Prepare Steps'
      modalContent.appendChild(prepareHeading)
      const prepareSteps = map.renderer.results.filter(
        r => r.type === 'prepare',
      )
      prepareSteps.forEach(printDetails)

      // Add contents about render steps
      const renderHeading = document.createElement('h3')
      renderHeading.textContent = 'Render Steps'
      modalContent.appendChild(renderHeading)
      const renderSteps = map.renderer.results.filter(r => r.type === 'render')
      renderSteps.forEach(printDetails)
    },
  })

/**
 * printObject. Generate <details> in parent element based on Ojbect properties
 *
 * @param {Object} obj
 * @param {HTMLElement} parentElement
 * @param {String} name
 */
function printObject (obj, parentElement, name = null) {
  // Create <details> and <summary> inside
  const detailsEle = document.createElement('details')
  const details = name ?? (obj instanceof Error ? obj.name : Object.values(obj)[0])
  detailsEle.innerHTML = `<summary>${details}</summary>`
  parentElement.appendChild(detailsEle)

  detailsEle.onclick = () => {
    // Don't add items if it has contents
    if (detailsEle.querySelector(':scope > :not(summary)')) return

    if (obj instanceof Error) {
      // Handle Error objects specially
      const errorProps = ['name', 'message', 'stack', ...Object.keys(obj)]
      errorProps.forEach(key => {
        const value = obj[key]
        const valueString = key === 'stack' ? `<pre>${value}</pre>` : value
        const propertyElement = document.createElement('p')
        propertyElement.innerHTML = `<strong>${key}</strong>: ${valueString}`
        detailsEle.appendChild(propertyElement)
      })
    } else {
      // Handle regular objects
      Object.entries(obj).forEach(([key, value]) => {
        if (typeof value === 'object' && value !== null) {
          printObject(value, detailsEle, key)
        } else {
          const valueString =
            typeof value === 'function'
              ? `<pre>${value}</pre>`
              : value ?? typeof value
          const propertyElement = document.createElement('p')
          propertyElement.innerHTML = `<strong>${key}</strong>: ${valueString}`
          detailsEle.appendChild(propertyElement)
        }
      })
    }
  }
}

/**
 * toggleBlockFocus. Menu Item for toggling focus on a block
 *
 * @param {HTMLElement} block
 */
export const toggleBlockFocus = block =>
  Item({
    text: 'Toggle Focus',
    onclick: () => block.classList.toggle('focus'),
  })

/**
 * toggleMapFocus. Menu Item for toggling focus on a map
 *
 * @param {HTMLElement} map
 */
export const toggleMapFocus = map =>
  Item({
    text: 'Toggle Focus',
    onclick: () => {
      if (map.classList.toggle('focus')) {
        map.classList.add('focus-manual')
      }
    },
  })

/**
 * getCoordinatesByPixels.
 *
 * @param {HTMLElement} map instance
 * @param {Number[]} xy - pixel of window
 */
export const getCoordinatesByPixels = (map, xy) =>
  Item({
    text: 'Get Coordinates',
    onclick: () => {
      const [x, y] = map.renderer.unproject(xy)
      const xyString = `[${x.toFixed(7)}, ${y.toFixed(7)}]`
      navigator.clipboard.writeText(xyString)
      window.alert(`${xyString} copied to clipboard`)
    },
  })

/**
 * restoreCamera.
 *
 * @param {HTMLElement} map
 */
export const restoreCamera = map =>
  Item({
    text: 'Restore Camera',
    onclick: () => map.renderer.restoreCamera(),
  })

/**
 * addRefLink. replace selected text into markdown link by reference style links
 *
 * @param {CodeMirror} cm
 * @param {RefLink[]} refLinks
 */
export const addRefLink = (cm, refLinks) =>
  Folder({
    text: 'Add Link',
    items: refLinks.map(refLink => {
      let text = refLink.ref
      if (refLink.link.startsWith('geo:')) text = `@ ${text}`
      if (refLink.title?.match(/^=>/)) text = `=> ${text}`

      return Item({
        text,
        title: refLink.link,
        onclick: () => {
          const selection = cm.getSelection()
          if (selection === refLink.ref) {
            cm.replaceSelection(`[${selection}]`)
          } else {
            cm.replaceSelection(`[${selection}][${refLink.ref}]`)
          }
        },
      })
    }),
  })

/**
 * setGeoLinkTypeItem.
 *
 * @param {GeoLink} link
 * @param {String} text
 * @param {String} type
 */
export const setGeoLinkTypeItem = ({ link, type, ...others }) => {
  const params = new URLSearchParams(link.search)
  return Item({
    ...others,
    className: ['keep-menu'],
    onclick: () => {
      params.set('type', type)
      link.search = params
      removeLeaderLines(link)
      getMarkersFromMaps(link)
        .forEach(marker => marker.remove())
      getMarkersFromMaps(link)
    },
  })
}

/**
 * setGeoLinkType.
 *
 * @param {HTMLAnchorElement} link
 */
export const setGeoLinkType = (link) => Folder({
  text: 'Marker Type',
  style: 'min-width: unset; display: grid; grid-template-columns: repeat(5, 1fr);',
  items: Object.entries(markers)
    .sort(([, a], [, b]) => (a.order ?? 9999) > (b.order ?? 9999))
    .map(([key, value]) => {
      return setGeoLinkTypeItem({
        link,
        title: value.name ?? key.at(0).toUpperCase() + key.slice(1),
        innerHTML: value.html,
        type: key,
        style: 'min-width: unset; width: fit-content; padding: 10px; margin: auto auto;',
      })
    }),
})

/**
 * set type of leader-line
 *
 * @param {GeoLink | DocLink} link
 */
export const setLeaderLineType = (link) => Folder({
  text: 'Line Type',
  items: ['magnet', 'straight', 'grid', 'fluid']
    .map(path => Item({
      text: path,
      className: ['keep-menu'],
      onclick: () => {
        link.dataset.linePath = path
        removeLeaderLines(link)
        link.onmouseover()
      },
    })),
})

/**
 * addMarker.
 *
 * @param {Object} options
 * @param {HTMLElement} options.map - map element
 * @param {Number[]} options.point - xy values in pixel
 * @param {Function} options.isNameValid - check marker name is valid
 * @param {Function} options.callback
 */
export const addMarker = ({
  map,
  point,
  isNameValid = () => true,
  callback = null,
}) => Item({
  text: 'Add Marker',
  onclick: () => {
    let markerName
    do {
      markerName = window.prompt(markerName ? 'Name exists' : 'Marker Name')
    } while (markerName && !isNameValid(markerName))
    if (markerName === null) return

    const marker = addMarkerByPoint({ point, map, title: markerName })
    callback?.(marker)
  },
})

/**
 * editByRawText.
 *
 * @param {HTMLElement} map
 */
export const editMapByRawText = (map) => Item({
  text: 'Edit by Raw Text',
  onclick: () => {
    const container = map.closest('.map-container')
    const maps = Array.from(container.querySelectorAll('.mapclay'))
    if (!maps) return false

    const rect = map.getBoundingClientRect()
    const textArea = document.createElement('textarea')
    textArea.className = 'edit-map'
    textArea.style.cssText = `width: ${rect.width}px; height: ${rect.height}px;`
    textArea.value = maps.map(map => map.dataset.mapclay ?? '')
      .join('\n---\n')
      .replaceAll(',', '\n')
      .replaceAll(/["{}]/g, '')
      .replaceAll(/:(\w)/g, ': $1')

    textArea.addEventListener('focusout', () => {
      const code = document.createElement('code')
      code.className = 'map'
      code.textContent = textArea.value
      container.dataset.render = 'no-delay'
      textArea.replaceWith(code)
    })
    container.replaceChildren(textArea)

    return true
  },
})

export const editMap = (map, dumbymap) => {
  const options = Object.entries(dumbymap.aliases)
    .map(([option, aliases]) =>
      Folder({
        text: option,
        items: Object.entries(aliases)
          .map(([alias, value]) => {
            const aliasValue = value.value ?? value
            return Item({
              innerHTML: `<div>${alias}</div><div style="padding-left: 20px; color: gray; font-size: 1rem";">${aliasValue}</div>`,
              style: 'display: flex; justify-content: space-between; max-width: 20rem;',
              onclick: () => {
                const container = map.closest('.map-container')
                const configText = Array.from(container.querySelectorAll('.mapclay'))
                  .map(map => map.dataset.mapclay ?? '')
                  .join('\n---\n')
                const configList = parseConfigsFromYaml(configText)
                configList.find(config => config.id === map.id)[option] = aliasValue
                const code = document.createElement('code')
                code.className = 'map'
                code.textContent = configList.map(JSON.stringify).join('\n---\n')
                container.dataset.render = 'no-delay'
                container.replaceChildren(code)
              },
            })
          }),
      }),
    )
  return Folder({
    text: 'Edit Map',
    style: 'overflow: visible;',
    items: [
      editMapByRawText(map),
      ...options,
    ],
  })
}