import { indentLess } from '@codemirror/commands';
import { indentUnit } from '@codemirror/language';
import { EditorSelection, EditorState, Line, type ChangeSpec } from '@codemirror/state';
import { EditorView, type KeyBinding } from '@codemirror/view';

export const indentKeyBinding: KeyBinding = {
  key: 'Tab',
  run: indentMore,
  shift: indentLess,
};

function indentMore({ state, dispatch }: EditorView) {
  if (state.readOnly) {
    return false;
  }

  dispatch(
    state.update(
      changeBySelectedLine(state, (from, to, changes) => {
        changes.push({ from, to, insert: state.facet(indentUnit) });
      }),
      { userEvent: 'input.indent' },
    ),
  );

  return true;
}

function changeBySelectedLine(
  state: EditorState,
  cb: (from: number, to: number | undefined, changes: ChangeSpec[], line: Line) => void,
) {
  return state.changeByRange((range) => {
    const changes: ChangeSpec[] = [];

    const line = state.doc.lineAt(range.from);

    // just insert single indent unit at the current cursor position
    if (range.from === range.to) {
      cb(range.from, undefined, changes, line);
    }
    // handle the case when multiple characters are selected in a single line
    else if (range.from < range.to && range.to <= line.to) {
      cb(range.from, range.to, changes, line);
    } else {
      let atLine = -1;

      // handle the case when selection spans multiple lines
      for (let pos = range.from; pos <= range.to; ) {
        const line = state.doc.lineAt(pos);

        if (line.number > atLine && (range.empty || range.to > line.from)) {
          cb(line.from, undefined, changes, line);
          atLine = line.number;
        }

        pos = line.to + 1;
      }
    }

    const changeSet = state.changes(changes);

    return {
      changes,
      range: EditorSelection.range(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)),
    };
  });
}
