import type { Editor } from "@tiptap/core"
import type { Node as PMNode } from "@tiptap/pm/model"
import { NodeSelection, type EditorState, type ResolvedPos, type Transaction } from "@tiptap/pm/state"
import { parseGraphicAttrs } from "./docs-graphic-types.ts"

function isGraphicNode(node: PMNode): boolean {
  return node.type.name === "docsGraphic" || node.type.name === "docsInlineGraphic"
}

/** Document position immediately before child `childIndex` inside `$pos.parent`. */
export function childPosAtIndex($pos: ResolvedPos, childIndex: number): number {
  const parent = $pos.parent
  let pos = $pos.start($pos.depth)
  for (let i = 0; i < childIndex; i++) pos += parent.child(i).nodeSize
  return pos
}

function removeEmptyParagraph(tr: Transaction, paragraphPos: number): void {
  const mappedPos = tr.mapping.map(paragraphPos)
  const paragraph = tr.doc.nodeAt(mappedPos)
  if (paragraph?.type.name === "paragraph" && paragraph.content.size === 0) {
    tr.delete(mappedPos, mappedPos + paragraph.nodeSize)
  }
}

function inlineToBlockNode(state: EditorState, node: PMNode): PMNode | null {
  const blockType = state.schema.nodes.docsGraphic
  if (!blockType) return null
  const attrs = parseGraphicAttrs(node.attrs as Record<string, unknown>)
  const wrap = attrs.wrap === "inline" ? "square" : attrs.wrap
  return blockType.create({ ...node.attrs, placement: "block", wrap })
}

function findSelectedGraphic(
  state: EditorState
): { pos: number; node: PMNode } | null {
  const { selection } = state
  if (!(selection instanceof NodeSelection)) return null
  const node = selection.node
  if (!isGraphicNode(node)) return null
  return { pos: selection.from, node }
}

function moveBlockGraphic(
  state: EditorState,
  pos: number,
  node: PMNode,
  direction: "up" | "down",
  dispatch?: (tr: Transaction) => void
): boolean {
  const $pos = state.doc.resolve(pos)
  const index = $pos.index($pos.depth)
  const parent = $pos.parent

  if (direction === "up") {
    if (index === 0) return false
    const insertPos = childPosAtIndex($pos, index - 1)
    const tr = state.tr
    tr.delete(pos, pos + node.nodeSize)
    const mappedInsert = tr.mapping.map(insertPos)
    tr.insert(mappedInsert, node)
    tr.setSelection(NodeSelection.create(tr.doc, mappedInsert))
    dispatch?.(tr)
    return true
  }

  if (index >= parent.childCount - 1) return false
  const insertPos = childPosAtIndex($pos, index + 2)
  const tr = state.tr
  tr.delete(pos, pos + node.nodeSize)
  const mappedInsert = tr.mapping.map(insertPos)
  tr.insert(mappedInsert, node)
  tr.setSelection(NodeSelection.create(tr.doc, mappedInsert))
  dispatch?.(tr)
  return true
}

function moveInlineGraphic(
  state: EditorState,
  pos: number,
  node: PMNode,
  direction: "up" | "down",
  dispatch?: (tr: Transaction) => void
): boolean {
  const blockNode = inlineToBlockNode(state, node)
  if (!blockNode) return false

  const $pos = state.doc.resolve(pos)
  const paragraphPos = $pos.before($pos.depth)
  const $paragraph = state.doc.resolve(paragraphPos)
  if (direction === "up" && $paragraph.index($paragraph.depth) === 0) return false

  const paragraphEnd = $pos.after($pos.depth)
  if (direction === "down" && paragraphEnd >= state.doc.content.size) return false

  const tr = state.tr
  tr.delete(pos, pos + node.nodeSize)
  removeEmptyParagraph(tr, paragraphPos)

  const insertPos =
    direction === "up"
      ? tr.mapping.map(paragraphPos)
      : tr.mapping.map(paragraphEnd)

  tr.insert(insertPos, blockNode)
  tr.setSelection(NodeSelection.create(tr.doc, insertPos))
  dispatch?.(tr)
  return true
}

/** Move a move-with-text graphic up/down in the document flow. */
export function moveInFlowGraphic(editor: Editor, direction: "up" | "down"): boolean {
  const { state } = editor
  const selected = findSelectedGraphic(state)
  if (!selected) return false

  const { pos, node } = selected
  if (node.type.name === "docsGraphic") {
    return moveBlockGraphic(state, pos, node, direction, editor.view.dispatch.bind(editor.view))
  }
  return moveInlineGraphic(state, pos, node, direction, editor.view.dispatch.bind(editor.view))
}
