import type { Attrs, Node } from '../model';
import {
  blockById,
  caret,
  inlineLength,
  isAcrossBlocks,
  isCollapsed,
  nextBlock,
  nodeInline,
  nodeSelection,
  orderedSelection,
  previousBlock,
} from '../model';
import type { Command, WritekitState } from '../state';
import { createTransaction } from '../state';

/** Type/attrs for the block created when splitting `block` at `offset`. */
function continuation(state: WritekitState, block: Node, offset: number): { type?: string; attrs?: Attrs } {
  const spec = state.schema.nodeSpec(block.type);

  // Defining blocks (e.g. code-block) keep their identity across a split.
  if (spec?.defining)
    return { type: block.type, attrs: block.attrs };

  // Pressing Enter at the end of a heading starts a fresh paragraph.
  if (block.type === 'heading' && offset >= inlineLength(nodeInline(block)) && state.registry.hasBlock('paragraph'))
    return { type: 'paragraph' };

  // A new to-do item always starts unchecked (don't inherit the checked state).
  if (block.type === 'todo-list')
    return { type: block.type, attrs: { ...block.attrs, checked: false } };

  // Otherwise continue the same type (lists keep their indent/listType attrs).
  return { type: block.type, attrs: block.attrs };
}

/**
 * Split the current text block at the caret (Enter). A non-collapsed same-block
 * selection is deleted first. Caret lands at the start of the new block.
 */
export const splitBlock: Command = (state, dispatch) => {
  const sel = state.selection;

  if (sel.kind !== 'text' || isAcrossBlocks(sel))
    return false;

  const block = blockById(state.doc, sel.focus.blockId);
  const spec = block && state.schema.nodeSpec(block.type);

  if (!block || spec?.content.kind !== 'text')
    return false;

  // Code blocks never split — Enter inserts a literal newline.
  if (spec.code) {
    if (dispatch) {
      const tr = createTransaction(state);
      let pos = sel.focus;

      if (!isCollapsed(sel)) {
        const { from, to } = orderedSelection(sel, state.doc);
        tr.deleteText(block.id, from.offset, to.offset);
        pos = from;
      }

      tr.insertText(pos, '\n', []).setSelection(caret(block.id, pos.offset + 1));
      dispatch(tr);
    }

    return true;
  }

  if (dispatch) {
    const tr = createTransaction(state);
    let pos = sel.focus;

    if (!isCollapsed(sel)) {
      const { from, to } = orderedSelection(sel, state.doc);
      tr.deleteText(block.id, from.offset, to.offset);
      pos = from;
    }

    const cont = continuation(state, block, pos.offset);
    tr.splitBlock(pos, cont.type, cont.attrs);
    tr.setSelection(caret(tr.lastSplitId!, 0));
    dispatch(tr);
  }

  return true;
};

/** Insert a hard line break (Shift+Enter) inside the current block. */
export const insertHardBreak: Command = (state, dispatch) => {
  const sel = state.selection;

  if (sel.kind !== 'text' || !isCollapsed(sel))
    return false;

  const block = blockById(state.doc, sel.focus.blockId);
  const spec = block && state.schema.nodeSpec(block.type);

  if (!block || spec?.content.kind !== 'text')
    return false;

  if (dispatch) {
    dispatch(createTransaction(state)
      .insertText(sel.focus, '\n', state.storedMarks ?? [])
      .setSelection(caret(block.id, sel.focus.offset + 1)));
  }

  return true;
};

/**
 * Backspace at the start of a block: merge it into the previous text block, or
 * select a preceding atom block (image/divider) so a second Backspace deletes it.
 */
export const joinBackward: Command = (state, dispatch) => {
  const sel = state.selection;

  if (sel.kind !== 'text' || !isCollapsed(sel) || sel.focus.offset !== 0)
    return false;

  const current = blockById(state.doc, sel.focus.blockId);
  const prev = previousBlock(state.doc, sel.focus.blockId);

  if (!current || !prev)
    return false;

  const currentSpec = state.schema.nodeSpec(current.type);
  const prevSpec = state.schema.nodeSpec(prev.type);

  if (currentSpec?.isolating || prevSpec?.isolating)
    return false;

  if (prevSpec?.content.kind !== 'text') {
    if (dispatch)
      dispatch(createTransaction(state).setSelection(nodeSelection([prev.id])));
    return true;
  }

  if (currentSpec?.content.kind !== 'text')
    return false;

  if (dispatch) {
    const caretOffset = inlineLength(nodeInline(prev));
    dispatch(createTransaction(state).mergeBlock(current.id, prev.id).setSelection(caret(prev.id, caretOffset)));
  }

  return true;
};

/** Delete at the end of a block: merge the next text block into it. */
export const joinForward: Command = (state, dispatch) => {
  const sel = state.selection;

  if (sel.kind !== 'text' || !isCollapsed(sel))
    return false;

  const current = blockById(state.doc, sel.focus.blockId);

  if (!current || sel.focus.offset !== inlineLength(nodeInline(current)))
    return false;

  const next = nextBlock(state.doc, current.id);
  if (!next)
    return false;

  const currentSpec = state.schema.nodeSpec(current.type);
  const nextSpec = state.schema.nodeSpec(next.type);

  if (currentSpec?.isolating || nextSpec?.isolating || currentSpec?.content.kind !== 'text' || nextSpec?.content.kind !== 'text')
    return false;

  if (dispatch) {
    const caretOffset = inlineLength(nodeInline(current));
    dispatch(createTransaction(state).mergeBlock(next.id, current.id).setSelection(caret(current.id, caretOffset)));
  }

  return true;
};
