import {isTextBlock} from '@portabletext/schema'
import {
  deleteText,
  Editor,
  Element,
  Range,
  setSelection,
  Transforms,
} from 'slate'
import {DOMEditor} from 'slate-dom'
import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
import {getBlockPath} from '../internal-utils/slate-utils'
import {toSlateRange} from '../internal-utils/to-slate-range'
import {getBlockKeyFromSelectionPoint} from '../selection/selection-point'
import {PortableTextSlateEditor} from '../types/editor'
import type {BehaviorOperationImplementation} from './behavior.operations'

export const deleteOperationImplementation: BehaviorOperationImplementation<
  'delete'
> = ({context, operation}) => {
  const anchorBlockKey = getBlockKeyFromSelectionPoint(operation.at.anchor)
  const focusBlockKey = getBlockKeyFromSelectionPoint(operation.at.focus)

  const startBlockKey = operation.at.backward ? focusBlockKey : anchorBlockKey
  const endBlockKey = operation.at.backward ? anchorBlockKey : focusBlockKey
  const endOffset = operation.at.backward
    ? operation.at.focus.offset
    : operation.at.anchor.offset

  if (!startBlockKey) {
    throw new Error('Failed to get start block key')
  }

  if (!endBlockKey) {
    throw new Error('Failed to get end block key')
  }

  const startBlockIndex = operation.editor.blockIndexMap.get(startBlockKey)

  if (startBlockIndex === undefined) {
    throw new Error('Failed to get start block index')
  }

  const startBlock = operation.editor.value.at(startBlockIndex)

  if (!startBlock) {
    throw new Error('Failed to get start block')
  }

  const endBlockIndex = operation.editor.blockIndexMap.get(endBlockKey)

  if (endBlockIndex === undefined) {
    throw new Error('Failed to get end block index')
  }

  const endBlock = operation.editor.value.at(endBlockIndex)

  if (!endBlock) {
    throw new Error('Failed to get end block')
  }

  const anchorBlockPath =
    anchorBlockKey !== undefined
      ? getBlockPath({
          editor: operation.editor,
          _key: anchorBlockKey,
        })
      : undefined
  const focusBlockPath =
    focusBlockKey !== undefined
      ? getBlockPath({
          editor: operation.editor,
          _key: focusBlockKey,
        })
      : undefined

  if (
    operation.at.anchor.path.length === 1 &&
    operation.at.focus.path.length === 1 &&
    anchorBlockPath &&
    focusBlockPath &&
    anchorBlockPath[0] === focusBlockPath[0]
  ) {
    Transforms.removeNodes(operation.editor, {
      at: [anchorBlockPath[0]],
    })

    if (operation.editor.children.length === 0) {
      Transforms.insertNodes(operation.editor, createPlaceholderBlock(context))
    }

    return
  }

  const range = toSlateRange({
    context: {
      schema: context.schema,
      value: operation.editor.value,
      selection: operation.at,
    },
    blockIndexMap: operation.editor.blockIndexMap,
  })

  if (!range) {
    throw new Error(
      `Failed to get Slate Range for selection ${JSON.stringify(operation.at)}`,
    )
  }

  if (operation.direction === 'backward' && operation.unit === 'line') {
    const parentBlockEntry = Editor.above(operation.editor, {
      match: (n) => Element.isElement(n) && Editor.isBlock(operation.editor, n),
      at: range,
    })

    if (parentBlockEntry) {
      const [, parentBlockPath] = parentBlockEntry
      const parentElementRange = Editor.range(
        operation.editor,
        parentBlockPath,
        range.anchor,
      )

      const currentLineRange = findCurrentLineRange(
        operation.editor,
        parentElementRange,
      )

      if (!Range.isCollapsed(currentLineRange)) {
        Transforms.delete(operation.editor, {at: currentLineRange})
        return
      }
    }
  }

  const hanging = isTextBlock(context, endBlock) && endOffset === 0

  deleteText(operation.editor, {
    at: range,
    reverse: operation.direction === 'backward',
    unit: operation.unit,
    hanging,
  })

  if (
    operation.editor.selection &&
    isTextBlock(context, startBlock) &&
    isTextBlock(context, endBlock)
  ) {
    setSelection(operation.editor, {
      anchor: operation.editor.selection.focus,
      focus: operation.editor.selection.focus,
    })
  }
}

function findCurrentLineRange(
  editor: PortableTextSlateEditor,
  parentRange: Range,
): Range {
  const parentRangeBoundary = Editor.range(editor, Range.end(parentRange))
  const positions = Array.from(Editor.positions(editor, {at: parentRange}))

  let left = 0
  let right = positions.length
  let middle = Math.floor(right / 2)

  if (
    rangesAreOnSameLine(
      editor,
      Editor.range(editor, positions[left]),
      parentRangeBoundary,
    )
  ) {
    return Editor.range(editor, positions[left], parentRangeBoundary)
  }

  if (positions.length < 2) {
    return Editor.range(
      editor,
      positions[positions.length - 1],
      parentRangeBoundary,
    )
  }

  while (middle !== positions.length && middle !== left) {
    if (
      rangesAreOnSameLine(
        editor,
        Editor.range(editor, positions[middle]),
        parentRangeBoundary,
      )
    ) {
      right = middle
    } else {
      left = middle
    }

    middle = Math.floor((left + right) / 2)
  }

  return Editor.range(editor, positions[left], parentRangeBoundary)
}

function rangesAreOnSameLine(editor: DOMEditor, range1: Range, range2: Range) {
  const rect1 = DOMEditor.toDOMRange(editor, range1).getBoundingClientRect()
  const rect2 = DOMEditor.toDOMRange(editor, range2).getBoundingClientRect()

  return domRectsIntersect(rect1, rect2) && domRectsIntersect(rect2, rect1)
}

function domRectsIntersect(rect: DOMRect, compareRect: DOMRect) {
  const middle = (compareRect.top + compareRect.bottom) / 2

  return rect.top <= middle && rect.bottom >= middle
}
