import {
  Annotation,
  Compartment,
  EditorSelection,
  EditorState,
  StateEffect,
  StateField,
  Transaction,
} from '@codemirror/state'
import {
  Decoration,
  DecorationSet,
  EditorView,
  ViewPlugin,
} from '@codemirror/view'
import { syntaxTree } from '@codemirror/language'
import { toggleVisualEffect } from '../../extensions/visual/visual'
import { hasLanguageLoadedEffect } from '../../extensions/language'

// to enable: window.localStorage.setItem('cm6-dev-tools', 'on')
// to disable: window.localStorage.removeItem('cm6-dev-tools')
const enabled = window.localStorage.getItem('cm6-dev-tools') === 'on'

const devToolsConf = new Compartment()

export const codemirrorDevTools = () => {
  return enabled ? [devToolsButton, devToolsConf.of(createExtension())] : []
}

const devToolsButton = ViewPlugin.define(view => {
  const getContainer = () =>
    document.querySelector('.formatting-buttons-wrapper')

  const removeButton = () => {
    getContainer()?.querySelector('#cm6-dev-tools-button')?.remove()
  }

  const addButton = () => {
    const button = document.createElement('button')
    button.classList.add('btn', 'formatting-btn', 'formatting-btn--icon')
    button.id = 'cm6-dev-tools-button'
    button.textContent = '🦧'
    button.addEventListener('click', event => {
      event.preventDefault()
      view.dispatch(toggleDevTools())
    })

    getContainer()?.prepend(button)
  }

  removeButton()
  addButton()

  return {
    update(update) {
      for (const tr of update.transactions) {
        for (const effect of tr.effects) {
          if (effect.is(toggleVisualEffect)) {
            window.setTimeout(() => {
              removeButton()
              addButton()
            })
          }
        }
      }
    },
    destroy() {
      removeButton()
    },
  }
})

const isActive = () =>
  window.localStorage.getItem('cm6-dev-tools-active') === 'on'

const toggleDevTools = () => {
  window.localStorage.setItem('cm6-dev-tools-active', isActive() ? 'off' : 'on')

  return {
    effects: devToolsConf.reconfigure(createExtension()),
  }
}

const createExtension = () =>
  isActive() ? [devToolsView, highlightSelectedNode, devToolsTheme] : []

const devToolsTheme = EditorView.baseTheme({
  '.ol-cm-dev-tools-container': {
    padding: '8px 8px 0',
    backgroundColor: '#222',
    color: '#eee',
    fontSize: '13px',
    flexShrink: '0',
    fontFamily: '"SF Mono", monospace',
    height: '100%',
    overflow: 'auto',
    position: 'sticky',
    top: 0,
  },
  '.ol-cm-dev-tools-item': {
    cursor: 'pointer',
    borderTop: '2px solid transparent',
    borderBottom: '2px solid transparent',
    scrollMargin: '2em',
  },
  '.ol-cm-selected-node-highlight': {
    backgroundColor: 'yellow',
  },
  '.ol-cm-dev-tools-covered-item': {
    backgroundColor: 'rgba(255, 255, 0, 0.2)',
  },
  '.ol-cm-dev-tools-selected-item': {
    backgroundColor: 'rgba(255, 255, 0, 0.5)',
    color: '#000',
  },
  '.ol-cm-dev-tools-cursor-before': {
    borderTopColor: 'rgba(255, 255, 0, 1)',
    '& + .ol-cm-dev-tools-cursor-before': {
      borderTopColor: 'transparent',
    },
  },
  '.ol-cm-dev-tools-positions': {
    position: 'sticky',
    bottom: '0',
    backgroundColor: 'inherit',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'flex-end',
  },
  '.ol-cm-dev-tools-position': {
    padding: '4px 0',
  },
})

const fromDevTools = Annotation.define()

const transactionIsFromDevTools = (tr: Transaction) =>
  tr.annotation(fromDevTools)

const devToolsView = ViewPlugin.define(view => {
  const scroller = document.querySelector<HTMLDivElement>('.cm-scroller')

  if (!scroller) {
    return {}
  }

  const container = document.createElement('div')
  container.classList.add('ol-cm-dev-tools-container')
  scroller.append(container)

  const highlightNodeRange = (from: number, to: number) => {
    view.dispatch({
      effects: [selectedNodeEffect.of({ from, to })],
    })
  }

  const selectNodeRange = (from: number, to: number) => {
    view.dispatch({
      annotations: [fromDevTools.of(true)],
      selection: EditorSelection.single(from, to),
      effects: EditorView.scrollIntoView(from, { y: 'center' }),
    })
    view.focus()
  }

  buildPanel(view.state, container, highlightNodeRange, selectNodeRange, true)

  return {
    update(update) {
      if (
        update.docChanged ||
        update.selectionSet ||
        hasLanguageLoadedEffect(update)
      ) {
        const scroll = !update.transactions.some(transactionIsFromDevTools)
        buildPanel(
          update.state,
          container,
          highlightNodeRange,
          selectNodeRange,
          scroll
        )
      }
    },
    destroy() {
      container.remove()
    },
  }
})

const buildPanel = (
  state: EditorState,
  container: HTMLDivElement,
  highlightNodeRange: (from: number, to: number) => void,
  selectNodeRange: (from: number, to: number) => void,
  scroll: boolean
) => {
  container.textContent = '' // clear

  const tree = syntaxTree(state)
  const { selection } = state
  let itemToCenter: HTMLDivElement

  let depth = 0
  tree.iterate({
    enter(nodeRef) {
      const { from, to, name } = nodeRef

      const element = document.createElement('div')
      element.classList.add('ol-cm-dev-tools-item')
      element.style.paddingLeft = `${depth * 16}px`
      element.textContent = name

      element.addEventListener('mouseover', () => {
        highlightNodeRange(from, to)
      })

      element.addEventListener('click', () => {
        selectNodeRange(from, to)
      })

      container.append(element)

      for (const range of selection.ranges) {
        // completely covered by selection
        if (range.from <= from && range.to >= to) {
          element.classList.add('ol-cm-dev-tools-selected-item')
          itemToCenter = element
        } else if (
          (range.from > from && range.from < to) ||
          (range.to > from && range.to < to)
        ) {
          element.classList.add('ol-cm-dev-tools-covered-item')
          itemToCenter = element
        }

        if (range.head === from) {
          element.classList.add('ol-cm-dev-tools-cursor-before')
          itemToCenter = element
        }
      }
      depth++
    },
    leave(node) {
      depth--
    },
  })

  const positions = document.createElement('div')
  positions.classList.add('ol-cm-dev-tools-positions')
  container.append(positions)

  for (const range of state.selection.ranges) {
    const line = state.doc.lineAt(range.head)
    const column = range.head - line.from + 1
    const position = document.createElement('div')
    position.classList.add('ol-cm-dev-tools-position')
    position.textContent = `line ${line.number}, col ${column}, pos ${range.head}`
    positions.append(position)
  }

  if (scroll && itemToCenter!) {
    window.setTimeout(() => {
      itemToCenter.scrollIntoView({
        block: 'center',
        inline: 'center',
      })
    })
  }
}

const selectedNodeEffect = StateEffect.define<{
  from: number
  to: number
} | null>()

const highlightSelectedNode = StateField.define<DecorationSet>({
  create() {
    return Decoration.none
  },
  update(value, tr) {
    if (tr.selection) {
      value = Decoration.none
    }
    for (const effect of tr.effects) {
      if (effect.is(selectedNodeEffect)) {
        if (effect.value) {
          const { from, to } = effect.value

          // TODO: widget decoration if no range to decorate?
          if (to > from) {
            value = Decoration.set([
              Decoration.mark({
                class: 'ol-cm-selected-node-highlight',
              }).range(from, to),
            ])
          }
        } else {
          value = Decoration.none
        }
      }
    }
    return value
  },
  provide(f) {
    return EditorView.decorations.from(f)
  },
})
