import { EditorState, Selection } from 'prosemirror-state'
import type { Node, Schema, ResolvedPos } from 'prosemirror-model'
import { InputRule, inputRules } from 'prosemirror-inputrules'
import { keymap } from 'prosemirror-keymap'
import type { ProseMirrorExtension } from '../state'

export const tableInputRule = (schema: Schema) =>
  new InputRule(
    new RegExp('^\\|{2,}\\s$'),
    (state: EditorState, match: string[], start: number, end: number) => {
      const tr = state.tr
      const columns = [...Array.from({ length: match[0].trim().length - 1 })]
      const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
      const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
      const table = schema.node(schema.nodes.table, {}, [
        schema.node(schema.nodes.table_head, {}, schema.node(schema.nodes.table_row, {}, headers)),
        schema.node(schema.nodes.table_body, {}, schema.node(schema.nodes.table_row, {}, cells))
      ])

      tr.delete(start - 1, end)
      tr.insert(start - 1, table)
      tr.setSelection(Selection.near(tr.doc.resolve(start + 3)))
      return tr
    }
  )

const tableSchema = {
  table: {
    content: '(table_head | table_body)*',
    isolating: true,
    selectable: false,
    group: 'block',
    parseDOM: [{ tag: 'div[data-type="table"]' }],
    toDOM: () => [
      'div',
      {
        class: 'table-container',
        'data-type': 'table'
      },
      ['table', 0]
    ]
  },
  table_head: {
    content: 'table_row',
    isolating: true,
    group: 'table_block',
    selectable: false,
    parseDOM: [{ tag: 'thead' }],
    toDOM: () => ['thead', 0]
  },
  table_body: {
    content: 'table_row+',
    isolating: true,
    group: 'table_block',
    selectable: false,
    parseDOM: [{ tag: 'tbody' }],
    toDOM: () => ['tbody', 0]
  },
  table_row: {
    content: '(table_cell | table_header)*',
    parseDOM: [{ tag: 'tr' }],
    toDOM: () => ['tr', 0]
  },
  table_cell: {
    content: 'inline*',
    isolating: true,
    group: 'table_block',
    selectable: false,
    attrs: { style: { default: null } },
    parseDOM: [
      {
        tag: 'td',
        getAttrs: (dom: HTMLElement) => {
          const textAlign = dom.style.textAlign
          return textAlign ? { style: `text-align: ${textAlign}` } : null
        }
      }
    ],
    toDOM: (node: Node) => ['td', node.attrs, 0]
  },
  table_header: {
    content: 'inline*',
    isolating: true,
    group: 'table_block',
    selectable: false,
    attrs: { style: { default: null } },
    parseDOM: [
      {
        tag: 'th',
        getAttrs: (dom: HTMLElement) => {
          const textAlign = dom.style.textAlign
          return textAlign ? { style: `text-align: ${textAlign}` } : null
        }
      }
    ],
    toDOM: (node: Node) => ['th', node.attrs, 0]
  }
}

const findParentPos = ($pos: ResolvedPos, fn: (n: Node) => boolean) => {
  for (let d = $pos.depth; d > 0; d--) {
    if (fn($pos.node(d))) return $pos.doc.resolve($pos.before(d + 1))
  }
  return null
}

const findTableCellPos = ($pos: ResolvedPos, header = true) =>
  findParentPos($pos, (n) => n.type.name === 'table_cell' || (header && n.type.name === 'table_header'))

const findTableRowPos = ($pos: ResolvedPos) => findParentPos($pos, (n) => n.type.name === 'table_row')

const findTableHeadPos = ($pos: ResolvedPos) => findParentPos($pos, (n) => n.type.name === 'table_head')

const findTablePos = ($pos: ResolvedPos) => findParentPos($pos, (n) => n.type.name === 'table')

const findNodePosition = (node: Node, fn: (n: Node, p: Node) => boolean) => {
  let result = -1
  node.descendants((n, pos, p) => {
    if (result !== -1) {
      return false
    } else if (fn(n, p)) {
      result = pos
      return false
    }
  })

  return result
}

const findVertTableCellPos = ($pos: ResolvedPos, dir = 'up') => {
  const cellPos = findTableCellPos($pos)
  const rowPos = findTableRowPos($pos)
  const offset = cellPos.pos - ($pos.before() + 1)

  const add = dir === 'up' ? -1 : rowPos.node().nodeSize + 1
  const nodeBeforePos = $pos.doc.resolve(rowPos.before() + add)
  let rowBeforePos = findTableRowPos(nodeBeforePos)

  if (!rowBeforePos) {
    const table = $pos.node(0)
    const tablePos = findTablePos($pos)
    const inTableHead = !!findTableHeadPos($pos)

    if (dir === 'up' && inTableHead) {
      return $pos.doc.resolve(Math.max(0, tablePos.before() - 1))
    } else if (dir === 'down' && !inTableHead) {
      return $pos.doc.resolve(tablePos.after())
    }

    const pos = findNodePosition(table, (n, p) => {
      return inTableHead
        ? p.type.name === 'table_body' && n.type.name === 'table_row'
        : p.type.name === 'table_head' && n.type.name === 'table_row'
    })

    rowBeforePos = $pos.doc.resolve(pos + 1)
  }

  const targetCell = $pos.doc.resolve(rowBeforePos.posAtIndex(rowPos.index()) + 1)
  const targetCellTextSize = getTextSize(targetCell.node())
  const cellOffset = offset > targetCellTextSize ? targetCellTextSize : offset
  return $pos.doc.resolve(targetCell.pos + cellOffset)
}

const getTextSize = (n: Node) => {
  let size = 0
  n.descendants((d: Node) => {
    size += d.text?.length ?? 0
  })

  return size
}

export default (): ProseMirrorExtension => ({
  schema: (prev) => ({
    ...prev,
    nodes: (prev.nodes as any).append(tableSchema)
  }),
  // FIXME (extract functions)
  // eslint-disable-next-line sonarjs/cognitive-complexity
  plugins: (prev, schema) => [
    keymap({
      'Ctrl-Enter': (state, dispatch) => {
        const tablePos = findTablePos(state.selection.$head)
        if (!tablePos) return false
        const targetPos = tablePos.after()
        const tr = state.tr
        tr.insert(targetPos, state.schema.node('paragraph'))
        tr.setSelection(Selection.near(tr.doc.resolve(targetPos)))
        dispatch(tr)
        return true
      },
      Backspace: (state, dispatch) => {
        const sel = state.selection
        if (!sel.empty) return false
        const cellPos = findTableCellPos(sel.$head)
        if (!cellPos) return false

        if (getTextSize(cellPos.node()) === 0) {
          const rowPos = findTableRowPos(sel.$head)
          const tablePos = findTablePos(sel.$head)
          const before = state.doc.resolve(cellPos.before() - 1)

          const cellBeforePos = findTableCellPos(before)
          const inTableHead = !!findTableHeadPos(sel.$head)

          if (cellBeforePos) {
            const tr = state.tr
            tr.setSelection(Selection.near(before))
            dispatch(tr)
            return true
          } else if (!inTableHead && getTextSize(rowPos.node()) === 0) {
            const tr = state.tr
            tr.delete(before.pos - 1, before.pos + rowPos.node().nodeSize)
            tr.setSelection(Selection.near(tr.doc.resolve(before.pos - 4)))
            dispatch(tr)
            return true
          } else if (getTextSize(tablePos.node()) === 0) {
            const tr = state.tr
            tr.delete(tablePos.before(), tablePos.before() + tablePos.node().nodeSize)
            dispatch(tr)
            return true
          }
        }

        return false
      },
      Enter: (state, dispatch) => {
        const sel = state.selection
        if (!sel.empty) return false
        const cellPos = findTableCellPos(sel.$head)
        if (!cellPos) return false

        const rowPos = findTableRowPos(sel.$head)
        const cells = []
        rowPos.node().forEach((cell) => {
          cells.push(schema.nodes.table_cell.create(cell.attrs))
        })
        const newRow = schema.nodes.table_row.create(null, cells)

        const theadPos = findTableHeadPos(sel.$head)
        if (theadPos) {
          const tablePos = findTablePos(sel.$head)
          let tbodyPos: number
          tablePos.node().descendants((node, pos) => {
            if (node.type.name === 'table_body') {
              tbodyPos = tablePos.pos + pos
            }
          })

          if (tbodyPos) {
            const tbody = state.doc.resolve(tbodyPos + 1)
            const tr = state.tr.insert(tbody.pos, newRow)
            tr.setSelection(Selection.near(tr.doc.resolve(tbody.pos)))
            dispatch(tr)
          } else {
            const tbody = schema.nodes.table_body.create(null, [newRow])
            const targetPos = theadPos.after()
            const tr = state.tr.insert(targetPos, tbody)
            tr.setSelection(Selection.near(tr.doc.resolve(targetPos)))
            dispatch(tr)
          }

          return true
        }

        const targetPos = sel.$head.after(-1)
        const tr = state.tr.insert(targetPos, newRow)
        tr.setSelection(Selection.near(tr.doc.resolve(targetPos)))

        dispatch(tr)
        return true
      },
      ArrowUp: (state, dispatch) => {
        const sel = state.selection
        if (!sel.empty) return false
        const cellPos = findTableCellPos(sel.$head)
        if (!cellPos) return false
        const abovePos = findVertTableCellPos(sel.$head)
        if (abovePos) {
          const tr = state.tr
          let selection = Selection.near(abovePos)
          if (abovePos.pos === 0 && cellPos.parentOffset === 0) {
            tr.insert(0, state.schema.node('paragraph'))
            selection = Selection.near(tr.doc.resolve(0))
          }
          tr.setSelection(selection)
          dispatch(tr)
          return true
        }

        return false
      },
      ArrowDown: (state, dispatch) => {
        const sel = state.selection
        if (!sel.empty) return false
        const cellPos = findTableCellPos(sel.$head)
        if (!cellPos) return false
        const belowPos = findVertTableCellPos(sel.$head, 'down')
        if (belowPos) {
          const tr = state.tr
          tr.setSelection(Selection.near(belowPos))
          dispatch(tr)
          return true
        }

        return false
      }
    }),
    ...prev,
    inputRules({ rules: [tableInputRule(schema)] })
  ]
})
