import {
  EditorView,
  showTooltip,
  Tooltip,
  TooltipView,
  keymap,
  ViewPlugin,
} from '@codemirror/view'
import {
  Extension,
  StateField,
  StateEffect,
  TransactionSpec,
  EditorSelection,
  Prec,
  Annotation,
} from '@codemirror/state'
import { closeAllContextMenusEffect } from '../utils/close-all-context-menus-effect'
import { isContextMenuMouseEvent } from '../utils/context-menu-mouse-event'
import { isMobileDevice } from '../utils/isMobileDevice'

const isMobile = isMobileDevice()

export const openContextMenuEffect = StateEffect.define<{
  pos: number
  x: number
  y: number
}>()

export const closeContextMenuEffect = StateEffect.define()

export const openContextMenuAnnotation = Annotation.define<boolean>()

type ContextMenuState = {
  tooltip: Tooltip | null
  mousePosition: { x: number; y: number } | null
}

export const contextMenuStateField = StateField.define<ContextMenuState>({
  create() {
    return { tooltip: null, mousePosition: null }
  },

  update(field, tr) {
    let next = field

    // Process effects in order but let open win if present in the same transaction
    for (const effect of tr.effects) {
      if (
        effect.is(closeContextMenuEffect) ||
        effect.is(closeAllContextMenusEffect)
      ) {
        next = { tooltip: null, mousePosition: null }
      }
      if (effect.is(openContextMenuEffect)) {
        const { pos, x, y } = effect.value
        return {
          tooltip: buildContextMenuTooltip(pos, { x, y }),
          mousePosition: { x, y },
        }
      }
    }

    // If effects changed the state, return early so doc-change fallback doesn’t override it
    if (next !== field) {
      return next
    }

    // Close menu on document changes
    if (tr.docChanged && field.tooltip) {
      return { tooltip: null, mousePosition: null }
    }

    return field
  },

  // Connect state field to tooltip system
  provide: field => [
    showTooltip.compute([field], state => state.field(field).tooltip),
  ],
})

function buildContextMenuTooltip(
  pos: number,
  mousePosition: { x: number; y: number }
): Tooltip {
  return {
    pos,
    above: false,
    strictSide: false,
    arrow: false,
    create: () => createTooltipView(mousePosition),
  }
}

const createTooltipView = (mousePosition: {
  x: number
  y: number
}): TooltipView => {
  const dom = document.createElement('div')
  dom.className = 'editor-context-menu-container'

  // Watch for size changes and reposition accordingly
  const resizeObserver = new ResizeObserver(() => {
    requestAnimationFrame(() => positionMenu(dom, mousePosition))
  })
  resizeObserver.observe(dom)

  return {
    dom,
    overlap: true,
    offset: { x: 0, y: 0 },
    destroy() {
      resizeObserver.disconnect()
    },
  }
}

function positionMenu(
  dom: HTMLElement,
  mousePosition: { x: number; y: number }
) {
  const bounds = dom.getBoundingClientRect()

  // Wait for menu to render
  if (bounds.width === 0 || bounds.height === 0) {
    return
  }

  const viewportWidth = window.innerWidth
  const viewportHeight = window.innerHeight
  const y = mousePosition.y

  // Adjust horizontal position if menu would overflow right edge
  let left = mousePosition.x
  if (mousePosition.x + bounds.width > viewportWidth) {
    left = viewportWidth - bounds.width
  }
  dom.style.setProperty('--context-menu-left', `${left}px`)
  const spaceBelow = viewportHeight - y
  let top = y
  if (bounds.height > spaceBelow) {
    // Show above if menu won't fit below
    top = y - bounds.height
  }

  dom.style.setProperty('--context-menu-top', `${top}px`)
}

function isPositionInsideSelection(pos: number, from: number, to: number) {
  return from !== to && pos >= from && pos <= to
}

function isPositionInsideAnyRangeOrCursor(view: EditorView, pos: number) {
  for (const range of view.state.selection.ranges) {
    // If it's a cursor (not a selection), only treat it as "inside" when
    // right-clicking exactly on the cursor position. This allows cursor
    // movement when clicking elsewhere on the same line.
    if (range.from === range.to) {
      if (pos === range.from) {
        return true
      }
      continue
    }

    if (isPositionInsideSelection(pos, range.from, range.to)) {
      return true
    }
  }
  return false
}

function selectEntireLine(
  view: EditorView,
  pos: number
): EditorSelection | null {
  if (pos === null) {
    return null
  }

  const line = view.state.doc.lineAt(pos)
  return EditorSelection.single(line.from, line.to)
}

function closeContextMenu(view: EditorView): void {
  const menuState = view.state.field(contextMenuStateField, false)
  if (menuState?.tooltip) {
    view.dispatch({ effects: closeContextMenuEffect.of(null) })
  }
}

function openContextMenuAtPosition(
  view: EditorView,
  pos: number,
  selection: EditorSelection | TransactionSpec['selection'],
  clientX: number,
  clientY: number
): void {
  view.dispatch({
    selection,
    effects: [
      closeAllContextMenusEffect.of(null),
      openContextMenuEffect.of({
        pos,
        x: clientX,
        y: clientY,
      }),
    ],
    annotations: [openContextMenuAnnotation.of(true)],
  })
}

function openContextMenuAtSelection(view: EditorView): boolean {
  const { main } = view.state.selection
  const pos = main.head
  const coords = view.coordsAtPos(pos)
  if (!coords) {
    return false
  }

  // Keep the current selection; actions should apply to it
  const selection = view.state.selection

  openContextMenuAtPosition(view, pos, selection, coords.left, coords.top)
  return true
}

function isClickOnGutter(target: HTMLElement): boolean {
  return !!target.closest('.cm-gutters')
}

// Gutter context menu plugin
const gutterContextMenuPlugin = (): Extension =>
  EditorView.updateListener.of(update => {
    if (!update.view.dom.parentElement) {
      return
    }

    const gutters = update.view.dom.parentElement.querySelector('.cm-gutters')
    // Attach listener only once per editor instance
    if (!gutters || gutters.hasAttribute('data-context-menu-attached')) {
      return
    }

    gutters.setAttribute('data-context-menu-attached', 'true')
    gutters.addEventListener('contextmenu', (event: Event) => {
      const mouseEvent = event as MouseEvent

      if (mouseEvent.shiftKey) {
        update.view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
        return
      }

      const pos = update.view.posAtCoords({
        x: mouseEvent.clientX,
        y: mouseEvent.clientY,
      })
      if (pos === null) {
        return
      }

      event.preventDefault()

      const selection = selectEntireLine(update.view, pos)
      if (selection) {
        openContextMenuAtPosition(
          update.view,
          pos,
          selection,
          mouseEvent.clientX,
          mouseEvent.clientY
        )
      }
    })
  })

// Handle right-click on ol-cm-filler (empty line widget)
// domEventHandlers doesn't fire for contenteditable="false" elements, so we use a direct DOM listener
const emptyLineFillerContextMenuPlugin = (): Extension =>
  ViewPlugin.define(view => {
    const contentDOM = view.contentDOM

    const handleContextMenu = (event: Event) => {
      const mouseEvent = event as MouseEvent
      const target = mouseEvent.target as HTMLElement

      // Only handle ol-cm-filler elements
      if (!target.classList.contains('ol-cm-filler')) {
        return
      }

      if (mouseEvent.shiftKey) {
        view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
        return
      }

      event.preventDefault()
      event.stopPropagation()

      // Re-dispatch on contentDOM so CodeMirror's domEventHandlers picks it up
      const customEvent = new MouseEvent('contextmenu', {
        bubbles: true,
        cancelable: true,
        clientX: mouseEvent.clientX,
        clientY: mouseEvent.clientY,
        shiftKey: mouseEvent.shiftKey,
      })
      contentDOM.dispatchEvent(customEvent)
    }

    contentDOM.addEventListener('contextmenu', handleContextMenu)

    return {
      destroy() {
        contentDOM.removeEventListener('contextmenu', handleContextMenu)
      },
    }
  })

// Editor view context menu handlers
const editorContextMenuHandlers = (): Extension =>
  EditorView.domEventHandlers({
    contextmenu(event: MouseEvent, view: EditorView) {
      if (event.shiftKey) {
        view.dispatch({ effects: closeAllContextMenusEffect.of(null) })
        return false
      }

      const pos = view.posAtCoords({ x: event.clientX, y: event.clientY })
      if (pos === null) {
        return false
      }

      event.preventDefault()

      const clickedInsideSelection = isPositionInsideAnyRangeOrCursor(view, pos)

      // Set cursor to clicked position if outside selection
      let selection: TransactionSpec['selection'] = { anchor: pos }
      if (clickedInsideSelection) {
        // Keep current selection if inside selection
        // so actions apply to the existing selection
        selection = view.state.selection
      }

      openContextMenuAtPosition(
        view,
        pos,
        selection,
        event.clientX,
        event.clientY
      )
      return true
    },

    mousedown(event: MouseEvent, view: EditorView) {
      const target = event.target as HTMLElement
      const isGutter = isClickOnGutter(target)
      const isRightClick = isContextMenuMouseEvent(event)

      // Close menu on any click except right-click on non-gutter
      if (!isRightClick || isGutter) {
        closeContextMenu(view)
      }

      // Prevent default on right-click to preserve selection,
      // but not when Shift is held (native context menu shortcut)
      if (isRightClick && !event.shiftKey) {
        event.preventDefault()
        return true
      }
      return false
    },
  })

// High-priority keymap to handle Escape before default handlers
const contextMenuKeymap = (): Extension =>
  Prec.high(
    keymap.of([
      {
        key: 'Escape',
        run: view => {
          const menuState = view.state.field(contextMenuStateField, false)
          if (menuState?.tooltip) {
            closeContextMenu(view)
            return true
          }
          return false
        },
      },
      {
        key: 'Shift-F10',
        // Accessibility standard shortcut to open context menu
        run: view => openContextMenuAtSelection(view),
      },
    ])
  )

export const contextMenu = (enabled: boolean): Extension =>
  enabled && !isMobile
    ? [
        contextMenuContainerTheme,
        contextMenuStateField,
        gutterContextMenuPlugin(),
        emptyLineFillerContextMenuPlugin(),
        editorContextMenuHandlers(),
        contextMenuKeymap(),
      ]
    : []

const contextMenuContainerTheme = EditorView.baseTheme({
  '.editor-context-menu-container.cm-tooltip': {
    backgroundColor: 'transparent',
    border: 'none',
    zIndex: 100,
    position: 'fixed !important',
    top: 'var(--context-menu-top) !important',
    left: 'var(--context-menu-left) !important',
  },
})
