import {
  Decoration,
  DecorationSet,
  EditorView,
  layer,
  LayerMarker,
  RectangleMarker,
  ViewPlugin,
  ViewUpdate,
} from '@codemirror/view'
import { sourceOnly } from './visual/visual'
import { fullHeightCoordsAtPos } from '../utils/layer'

/**
 * An alternative version of the built-in highlightActiveLine extension,
 * using a custom approach for highlighting the full height of the active “visual line” of a wrapped line.
 */
export const highlightActiveLine = (visual: boolean) => {
  // this extension should only be active in the source editor
  return sourceOnly(visual, [
    activeLineLayer,
    singleLineHighlighter,
    highlightActiveLineTheme,
  ])
}

const highlightActiveLineTheme = EditorView.baseTheme({
  '.ol-cm-activeLineLayer': {
    pointerEvents: 'none',
  },
})

/**
 * Line decoration approach used for non-wrapped lines, adapted from built-in
 * CodeMirror 6 highlightActiveLine, licensed under the MIT license:
 * https://github.com/codemirror/view/blob/main/src/active-line.ts
 */
const lineDeco = Decoration.line({ class: 'cm-activeLine' })

const singleLineHighlighter = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet

    constructor(view: EditorView) {
      this.decorations = this.getDeco(view)
    }

    update(update: ViewUpdate) {
      if (update.geometryChanged || update.selectionSet) {
        this.decorations = this.getDeco(update.view)
      }
    }

    getDeco(view: EditorView) {
      const deco = []

      // NOTE: only highlighting the active line for the main selection
      const { main } = view.state.selection

      // No active line highlight when text is selected
      if (main.empty) {
        const line = view.lineBlockAt(main.head)
        if (line.height <= view.defaultLineHeight) {
          deco.push(lineDeco.range(line.from))
        }
      }

      return Decoration.set(deco)
    }
  },
  {
    decorations: v => v.decorations,
  }
)

// Custom layer approach, used only for wrapped lines
const activeLineLayer = layer({
  above: false,
  class: 'ol-cm-activeLineLayer',
  markers(view: EditorView): readonly LayerMarker[] {
    const markers: LayerMarker[] = []

    // NOTE: only highlighting the active line for the main selection
    const { main } = view.state.selection

    // no active line highlight when text is selected
    if (!main.empty) {
      return markers
    }

    // Use line decoration when line doesn't wrap
    if (view.lineBlockAt(main.head).height <= view.defaultLineHeight) {
      return markers
    }

    const coords = fullHeightCoordsAtPos(
      view,
      main.head,
      main.assoc || undefined
    )

    if (coords) {
      const scrollRect = view.scrollDOM.getBoundingClientRect()
      const contentRect = view.contentDOM.getBoundingClientRect()
      const scrollTop = view.scrollDOM.scrollTop

      const top = coords.top - scrollRect.top + scrollTop
      const left = contentRect.left - scrollRect.left
      const width = contentRect.right - contentRect.left
      const height = coords.bottom - coords.top

      markers.push(
        new RectangleMarker('cm-activeLine', left, top, width, height)
      )
    }

    return markers
  },
  update(update: ViewUpdate): boolean {
    return update.geometryChanged || update.selectionSet
  },
})
